From 688dfc7fc026f3d28788494e3e4f5e466dec2f35 Mon Sep 17 00:00:00 2001 From: Werner Bihl Date: Fri, 10 Apr 2026 13:01:41 +0200 Subject: [PATCH 1/7] Refactor API to support sorting and pagination, update database integration, and add security vulnerability reporting guidelines --- SECURITY.md | 5 + TODO.txt | 4 +- src/index.ts | 393 +++++++++++++++++++++++++++++++++++--------------- wrangler.toml | 11 +- 4 files changed, 288 insertions(+), 125 deletions(-) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..cfadbf7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +## Reporting a Vulnerability + +Please contact admin@2dtiler.com with information about the security vulnerability. + +We highly appreciate any vulnerability feedback! diff --git a/TODO.txt b/TODO.txt index 1ec19d2..ca05ca0 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,3 @@ -CI/CD +Refactor +Our endpoint should also accept sorting and pagination Create API endpoint for fetching releases -https://lospec.com/palette-list/load?colorNumberFilterType=any&page=0&tag=&sortingType=newest \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 46af6cc..4243b85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,38 +5,231 @@ interface RateLimiter { } interface Env { - LOSPEC_PALETTES: KVNamespace; + DB: D1Database; RATE_LIMITER: RateLimiter; + INTERNAL_API_KEY: string; } -interface Palette { - name: string; - author: string; - colors: string[]; +interface LospecPaletteApiItem { + _id: string; + title?: string; + slug?: string; + description?: string; + tags?: unknown; + user?: unknown; + colors?: unknown; + examples?: unknown; + publishedAt?: string; } +interface LospecPaletteRow { + id: string; + title: string | null; + slug: string | null; + description: string | null; + tags: string | null; + user: string | null; + colors: string | null; + examples: string | null; + published_at: string | null; +} + +interface LospecPaletteResponse { + id: string; + title: string | null; + slug: string | null; + description: string | null; + tags: unknown | null; + user: unknown | null; + colors: unknown | null; + examples: unknown | null; + published_at: string | null; +} + +const ALLOWED_ORIGINS = new Set([ + "https://2dtiler.com", + "https://app.2dtiler.com", + "http://localhost:4321", + "http://127.0.0.1:4321", +]); +const INTERNAL_API_KEY_HEADER = "X-Internal-Api-Key"; +const LOSPEC_PALETTE_LIST_URL = "https://lospec.com/palette-list/load"; + const app = new Hono<{ Bindings: Env }>(); // ─── Helpers ──────────────────────────────────────────────────────────────── -/** Returns all keys from a KV namespace, transparently handling pagination. */ -async function listAllKeys(kv: KVNamespace): Promise { - const keys: string[] = []; - let cursor: string | undefined; +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} - do { - const result = await kv.list({ cursor }); - for (const key of result.keys) { - keys.push(key.name); - } - cursor = result.list_complete ? undefined : result.cursor; - } while (cursor !== undefined); +function isLospecPaletteApiItem(value: unknown): value is LospecPaletteApiItem { + return isRecord(value) && typeof value._id === "string"; +} + +function asNullableString(value: unknown): string | null { + return typeof value === "string" ? value : null; +} - return keys; +function serializeJsonField(value: unknown): string | null { + if (value === undefined || value === null) { + return null; + } + + return JSON.stringify(value); +} + +function deserializeJsonField(value: string | null): unknown | null { + if (!value) { + return null; + } + + try { + return JSON.parse(value) as unknown; + } catch { + return value; + } } -const sleep = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); +function mapPaletteToRow(palette: LospecPaletteApiItem): LospecPaletteRow { + return { + id: palette._id, + title: asNullableString(palette.title), + slug: asNullableString(palette.slug), + description: asNullableString(palette.description), + tags: serializeJsonField(palette.tags), + user: serializeJsonField(palette.user), + colors: serializeJsonField(palette.colors), + examples: serializeJsonField(palette.examples), + published_at: asNullableString(palette.publishedAt), + }; +} + +function mapRowToResponse(row: LospecPaletteRow): LospecPaletteResponse { + return { + id: row.id, + title: row.title, + slug: row.slug, + description: row.description, + tags: deserializeJsonField(row.tags), + user: deserializeJsonField(row.user), + colors: deserializeJsonField(row.colors), + examples: deserializeJsonField(row.examples), + published_at: row.published_at, + }; +} + +async function fetchLospecPalettePage( + page: number, + signal: AbortSignal, +): Promise { + const url = new URL(LOSPEC_PALETTE_LIST_URL); + url.searchParams.set("colorNumberFilterType", "any"); + url.searchParams.set("page", String(page)); + url.searchParams.set("tag", ""); + url.searchParams.set("sortingType", "newest"); + + const response = await fetch(url, { signal }); + if (!response.ok) { + throw new Error(`Failed to fetch page ${page}: HTTP ${response.status}`); + } + + const payload = (await response.json()) as { palettes?: unknown }; + if (!Array.isArray(payload.palettes)) { + return []; + } + + return payload.palettes.filter(isLospecPaletteApiItem); +} + +async function getExistingPaletteIds( + db: D1Database, + ids: string[], +): Promise> { + if (ids.length === 0) { + return new Set(); + } + + const placeholders = ids.map(() => "?").join(", "); + const statement = db.prepare( + `SELECT id FROM lospec_palettes WHERE id IN (${placeholders})`, + ); + const result = await statement.bind(...ids).all<{ id: string }>(); + + return new Set(result.results.map((row) => row.id)); +} + +async function insertPalettes( + db: D1Database, + palettes: LospecPaletteApiItem[], +): Promise { + if (palettes.length === 0) { + return 0; + } + + const statements = palettes.map((palette) => { + const row = mapPaletteToRow(palette); + + return db + .prepare( + `INSERT OR IGNORE INTO lospec_palettes ( + id, + title, + slug, + description, + tags, + user, + colors, + examples, + published_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + row.id, + row.title, + row.slug, + row.description, + row.tags, + row.user, + row.colors, + row.examples, + row.published_at, + ); + }); + + await db.batch(statements); + return palettes.length; +} + +function applyCorsHeaders(headers: Headers, origin: string): void { + headers.set("Access-Control-Allow-Origin", origin); + headers.set("Access-Control-Allow-Methods", "GET, OPTIONS"); + headers.set("Access-Control-Allow-Headers", "Content-Type"); + headers.set("Vary", "Origin"); +} + +app.use("*", async (c, next) => { + const origin = c.req.header("Origin"); + const internalApiKey = c.req.header(INTERNAL_API_KEY_HEADER); + + if (c.env.INTERNAL_API_KEY && internalApiKey === c.env.INTERNAL_API_KEY) { + await next(); + return; + } + + if (!origin || !ALLOWED_ORIGINS.has(origin)) { + return c.json({ error: "Forbidden origin" }, 403); + } + + if (c.req.method === "OPTIONS") { + const headers = new Headers(); + applyCorsHeaders(headers, origin); + return new Response(null, { status: 204, headers }); + } + + await next(); + applyCorsHeaders(c.res.headers, origin); +}); // ─── HTTP Routes ───────────────────────────────────────────────────────────── @@ -50,8 +243,7 @@ app.get("/", async (c) => { /** * GET /lospec_palettes - * Returns all fully-populated palette entries as a JSON array. - * Entries that are still pending fetch (stored as "{}") are omitted. + * Returns all stored Lospec palette entries from D1. */ app.get("/lospec_palettes", async (c) => { const ip = c.req.raw.headers.get("CF-Connecting-IP") ?? "unknown"; @@ -60,118 +252,83 @@ app.get("/lospec_palettes", async (c) => { return c.json({ error: "Rate limit exceeded. Try again in an hour." }, 429); } - const kv = c.env.LOSPEC_PALETTES; - const palettes: Palette[] = []; - const keys = await listAllKeys(kv); - - for (const key of keys) { - const value = await kv.get(key); - if (!value || value === "{}") continue; - - try { - const palette = JSON.parse(value) as Palette; - if (palette.name && Array.isArray(palette.colors)) { - palettes.push(palette); - } - } catch { - // skip malformed KV entries - } - } - - return c.json(palettes); + const result = await c.env.DB.prepare( + `SELECT + id, + title, + slug, + description, + tags, + user, + colors, + examples, + published_at + FROM lospec_palettes + ORDER BY published_at DESC, id DESC`, + ).all(); + + return c.json(result.results.map(mapRowToResponse)); }); // ─── Scheduled Job ─────────────────────────────────────────────────────────── /** - * Runs daily at 12:00 UTC (see wrangler.toml). + * Runs daily at 14:00 UTC (see wrangler.toml). * - * Phase 1 – Discover: fetch the Lospec sitemap and add any unknown palette - * slugs to KV as empty stubs ("{}"). - * - * Phase 2 – Populate: for every slug that still holds an empty stub, fetch - * the palette JSON from the Lospec API and persist the real data. - * A 3-second delay is inserted between requests to avoid flooding the server. + * Fetches Lospec palette pages from newest to oldest, inserts unseen rows into + * D1, and stops once a page contains any IDs that are already present. */ async function syncPalettes(env: Env, signal: AbortSignal): Promise { - // ── Phase 1: discover new slugs ────────────────────────────────────────── - - const sitemapRes = await fetch("https://lospec.com/sitemap.xml", { signal }); - if (!sitemapRes.ok) { - console.error(`Failed to fetch sitemap: ${sitemapRes.status}`); - return; - } - - const sitemapText = await sitemapRes.text(); - const slugs = new Set(); - const locRegex = /https:\/\/lospec\.com\/palette-list\/([^/g; - let match: RegExpExecArray | null; + let page = 0; + let pagesProcessed = 0; + let totalInserted = 0; - while ((match = locRegex.exec(sitemapText)) !== null) { - slugs.add(match[1]); - } - - console.log(`Sitemap: found ${slugs.size} palette URLs`); - - const existingKeys = new Set(await listAllKeys(env.LOSPEC_PALETTES)); - - let added = 0; - for (const slug of slugs) { - if (!existingKeys.has(slug)) { - await env.LOSPEC_PALETTES.put(slug, "{}"); - added++; - } - } - - console.log(`Seeded ${added} new palette stubs into KV`); - - // ── Phase 2: populate empty stubs ──────────────────────────────────────── - - // New slugs we just seeded are guaranteed to be "{}". - const newEmptySlugs = [...slugs].filter((s) => !existingKeys.has(s)); - - // Check pre-existing keys in parallel to find any leftover empty stubs from - // a previous partial run — avoids a redundant listAllKeys call and - // serialised per-key gets. - const existingEmptyKeys = ( - await Promise.all( - [...existingKeys].map(async (key) => { - const value = await env.LOSPEC_PALETTES.get(key); - return value === "{}" ? key : null; - }), - ) - ).filter((k): k is string => k !== null); - - const emptyKeys = [...existingEmptyKeys, ...newEmptySlugs]; - - console.log(`Fetching data for ${emptyKeys.length} empty palette entries`); - - for (let i = 0; i < emptyKeys.length; i++) { - if (signal.aborted) { - console.warn("Palette sync timed out; stopping early"); - break; - } + while (!signal.aborted) { + try { + const palettes = await fetchLospecPalettePage(page, signal); + if (palettes.length === 0) { + console.log(`Page ${page}: no palettes returned; stopping pagination`); + break; + } - if (i > 0) await sleep(3000); + const existingIds = await getExistingPaletteIds( + env.DB, + palettes.map((palette) => palette._id), + ); + const unseenPalettes = palettes.filter( + (palette) => !existingIds.has(palette._id), + ); + const inserted = await insertPalettes(env.DB, unseenPalettes); + + pagesProcessed++; + totalInserted += inserted; + + console.log( + `Page ${page}: fetched ${palettes.length}, inserted ${inserted}, existing ${existingIds.size}`, + ); + + if (existingIds.size > 0) { + console.log( + `Page ${page}: encountered existing palette IDs; stopping pagination`, + ); + break; + } - const slug = emptyKeys[i]; - try { - const res = await fetch(`https://lospec.com/palette-list/${slug}.json`, { - signal, - }); - if (res.ok) { - const data = await res.text(); - await env.LOSPEC_PALETTES.put(slug, data); - console.log(`✓ ${slug}`); - } else { - console.warn(`✗ ${slug} (HTTP ${res.status})`); + page++; + } catch (error) { + if (signal.aborted) { + console.warn("Palette sync timed out; stopping early"); + break; } - } catch (err) { - console.error(`✗ ${slug} (error):`, err); + + console.error(`Page ${page}: failed to sync palettes`, error); + return; } } - console.log("Palette sync complete"); + console.log( + `Palette sync complete: inserted ${totalInserted} palettes across ${pagesProcessed} page(s)`, + ); } // ─── Worker Export ─────────────────────────────────────────────────────────── diff --git a/wrangler.toml b/wrangler.toml index 254071d..5bdc745 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -6,14 +6,15 @@ compatibility_date = "2026-04-09" # The zone (2dtiler.com) must be on your Cloudflare account. route = { pattern = "api.2dtiler.com/*", zone_name = "2dtiler.com" } +[[d1_databases]] +binding = "DB" +database_name = "2dtiler" +database_id = "bd9d2b49-65a8-4684-9911-38c6095c6f0b" + [triggers] # Run daily at 14:00 UTC crons = ["0 14 * * *"] -[[kv_namespaces]] -binding = "LOSPEC_PALETTES" -id = "88adee5ffcfc4b36a3743f95fc18bc67" - # Rate limiting: 10 requests per hour per IP [[unsafe.bindings]] name = "RATE_LIMITER" @@ -21,7 +22,7 @@ type = "ratelimit" namespace_id = "1001" [unsafe.bindings.simple] limit = 10 -period = 3600 +period = 60 [observability] enabled = false From 6c2f880b3d268e6a75152193c48ee253c5960711 Mon Sep 17 00:00:00 2001 From: Werner Bihl Date: Fri, 10 Apr 2026 14:14:37 +0200 Subject: [PATCH 2/7] Implement API structure with health and Lospec palette routes, add CORS middleware, and integrate palette syncing functionality --- TODO.txt | 4 +- package.json | 3 +- src/app.ts | 15 + src/app/controllers/health.ts | 7 + src/app/controllers/lospec-palettes.ts | 64 ++++ src/app/middleware/cors.ts | 45 +++ src/app/models/lospec-palette.ts | 157 ++++++++ src/app/routes/health.ts | 8 + src/app/routes/lospec-palettes.ts | 8 + src/app/services/lospec-api.ts | 28 ++ .../services/lospec-palettes-repository.ts | 114 ++++++ src/config/constants.ts | 13 + src/config/types.ts | 11 + src/index.ts | 339 +----------------- src/jobs/sync-palettes.ts | 62 ++++ tests/integration/.gitkeep | 0 tests/unit/.gitkeep | 0 tsconfig.json | 2 +- 18 files changed, 542 insertions(+), 338 deletions(-) create mode 100644 src/app.ts create mode 100644 src/app/controllers/health.ts create mode 100644 src/app/controllers/lospec-palettes.ts create mode 100644 src/app/middleware/cors.ts create mode 100644 src/app/models/lospec-palette.ts create mode 100644 src/app/routes/health.ts create mode 100644 src/app/routes/lospec-palettes.ts create mode 100644 src/app/services/lospec-api.ts create mode 100644 src/app/services/lospec-palettes-repository.ts create mode 100644 src/config/constants.ts create mode 100644 src/config/types.ts create mode 100644 src/jobs/sync-palettes.ts create mode 100644 tests/integration/.gitkeep create mode 100644 tests/unit/.gitkeep diff --git a/TODO.txt b/TODO.txt index ca05ca0..47873a9 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,5 @@ -Refactor Our endpoint should also accept sorting and pagination + Create API endpoint for fetching releases +Add unit tests and add to CI/CD +Update README.md \ No newline at end of file diff --git a/package.json b/package.json index f6dc5c6..191d6b9 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy", - "lint": "eslint src" + "lint": "eslint src", + "typecheck": "tsc --noEmit" }, "dependencies": { "hono": "^4.12.12" diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..cc15cdd --- /dev/null +++ b/src/app.ts @@ -0,0 +1,15 @@ +import { Hono } from "hono"; + +import { registerHealthRoutes } from "./app/routes/health"; +import { registerLospecPaletteRoutes } from "./app/routes/lospec-palettes"; +import { corsMiddleware } from "./app/middleware/cors"; +import type { AppBindings } from "./config/types"; + +const app = new Hono(); + +app.use("*", corsMiddleware); + +registerHealthRoutes(app); +registerLospecPaletteRoutes(app); + +export default app; diff --git a/src/app/controllers/health.ts b/src/app/controllers/health.ts new file mode 100644 index 0000000..fc5bb4f --- /dev/null +++ b/src/app/controllers/health.ts @@ -0,0 +1,7 @@ +import type { Context } from "hono"; + +import type { AppBindings } from "../../config/types"; + +export function getHealth(c: Context): Response { + return c.json({}); +} diff --git a/src/app/controllers/lospec-palettes.ts b/src/app/controllers/lospec-palettes.ts new file mode 100644 index 0000000..f45a690 --- /dev/null +++ b/src/app/controllers/lospec-palettes.ts @@ -0,0 +1,64 @@ +import type { Context } from "hono"; + +import { LOSPEC_PALETTES_PAGE_SIZE } from "../../config/constants"; +import type { AppBindings } from "../../config/types"; +import { + mapRowToResponse, + type ListLospecPalettesOptions, +} from "../models/lospec-palette"; +import { listPalettes } from "../services/lospec-palettes-repository"; + +function parsePage(value: string | undefined): number | null { + if (value === undefined) { + return 0; + } + + if (!/^\d+$/.test(value)) { + return null; + } + + return Number.parseInt(value, 10); +} + +function normalizeQueryValue(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +function parseListLospecPalettesOptions( + query: Record, +): ListLospecPalettesOptions | null { + const page = parsePage(query.page); + if (page === null) { + return null; + } + + return { + page, + search: normalizeQueryValue(query.search), + tag: normalizeQueryValue(query.tags), + }; +} + +export async function getLospecPalettes( + c: Context, +): Promise { + const options = parseListLospecPalettesOptions(c.req.query()); + if (!options) { + return c.json( + { + error: `Invalid page parameter. Expected a non-negative integer with ${LOSPEC_PALETTES_PAGE_SIZE} results per page.`, + }, + 400, + ); + } + + const ip = c.req.raw.headers.get("CF-Connecting-IP") ?? "unknown"; + const { success } = await c.env.RATE_LIMITER.limit({ key: ip }); + if (!success) { + return c.json({ error: "Rate limit exceeded. Try again in an hour." }, 429); + } + + const palettes = await listPalettes(c.env.DB, options); + return c.json(palettes.map(mapRowToResponse)); +} diff --git a/src/app/middleware/cors.ts b/src/app/middleware/cors.ts new file mode 100644 index 0000000..c9f11ac --- /dev/null +++ b/src/app/middleware/cors.ts @@ -0,0 +1,45 @@ +import type { MiddlewareHandler } from "hono"; + +import { + ALLOWED_ORIGINS, + INTERNAL_API_KEY_HEADER, +} from "../../config/constants"; +import type { AppBindings } from "../../config/types"; + +function applyCorsHeaders(headers: Headers, origin: string): void { + headers.set("Access-Control-Allow-Origin", origin); + headers.set("Access-Control-Allow-Methods", "GET, OPTIONS"); + headers.set("Access-Control-Allow-Headers", "Content-Type"); + headers.set("Vary", "Origin"); +} + +export const corsMiddleware: MiddlewareHandler = async ( + c, + next, +) => { + const origin = c.req.header("Origin"); + const internalApiKey = c.req.header(INTERNAL_API_KEY_HEADER); + + if (c.env.INTERNAL_API_KEY && internalApiKey === c.env.INTERNAL_API_KEY) { + await next(); + return; + } + + if (!origin) { + await next(); + return; + } + + if (!ALLOWED_ORIGINS.has(origin)) { + return c.json({ error: "Forbidden origin" }, 403); + } + + if (c.req.method === "OPTIONS") { + const headers = new Headers(); + applyCorsHeaders(headers, origin); + return new Response(null, { status: 204, headers }); + } + + await next(); + applyCorsHeaders(c.res.headers, origin); +}; diff --git a/src/app/models/lospec-palette.ts b/src/app/models/lospec-palette.ts new file mode 100644 index 0000000..1899968 --- /dev/null +++ b/src/app/models/lospec-palette.ts @@ -0,0 +1,157 @@ +import { LOSPEC_CDN_BASE_URL } from "../../config/constants"; + +export interface LospecPaletteApiItem { + _id: string; + title?: string; + slug?: string; + description?: string; + tags?: unknown; + user?: unknown; + colors?: unknown; + examples?: unknown; + publishedAt?: string; +} + +export interface LospecPaletteRow { + id: string; + title: string | null; + slug: string | null; + description: string | null; + tags: string | null; + user: string | null; + colors: string | null; + examples: string | null; + published_at: string | null; +} + +export interface LospecPaletteExample { + image: string; + description: string | null; +} + +export interface LospecPaletteResponse { + id: string; + title: string | null; + slug: string | null; + description: string | null; + tags: unknown | null; + user: string | null; + colors: unknown | null; + examples: LospecPaletteExample[] | null; + published_at: string | null; +} + +export interface ListLospecPalettesOptions { + page: number; + search?: string; + tag?: string; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +export function isLospecPaletteApiItem( + value: unknown, +): value is LospecPaletteApiItem { + return isRecord(value) && typeof value._id === "string"; +} + +function asNullableString(value: unknown): string | null { + return typeof value === "string" ? value : null; +} + +function normalizePaletteUser(value: unknown): string | null { + if (typeof value === "string") { + return value; + } + + if (!isRecord(value)) { + return null; + } + + return asNullableString(value.name); +} + +function normalizePaletteExampleImage(value: string): string { + return new URL(value, LOSPEC_CDN_BASE_URL).toString(); +} + +function normalizePaletteExamples( + value: unknown, +): LospecPaletteExample[] | null { + if (!Array.isArray(value)) { + return null; + } + + return value.flatMap((example) => { + if (!isRecord(example)) { + return []; + } + + const image = asNullableString(example.image); + if (!image) { + return []; + } + + return [ + { + image: normalizePaletteExampleImage(image), + description: asNullableString(example.description), + }, + ]; + }); +} + +function serializeJsonField(value: unknown): string | null { + if (value === undefined || value === null) { + return null; + } + + return JSON.stringify(value); +} + +function deserializeJsonField(value: string | null): unknown | null { + if (!value) { + return null; + } + + try { + return JSON.parse(value) as unknown; + } catch { + return value; + } +} + +export function mapPaletteToRow( + palette: LospecPaletteApiItem, +): LospecPaletteRow { + return { + id: palette._id, + title: asNullableString(palette.title), + slug: asNullableString(palette.slug), + description: asNullableString(palette.description), + tags: serializeJsonField(palette.tags), + user: normalizePaletteUser(palette.user), + colors: serializeJsonField(palette.colors), + examples: serializeJsonField(normalizePaletteExamples(palette.examples)), + published_at: asNullableString(palette.publishedAt), + }; +} + +export function mapRowToResponse(row: LospecPaletteRow): LospecPaletteResponse { + const user = normalizePaletteUser(deserializeJsonField(row.user)); + const examples = normalizePaletteExamples(deserializeJsonField(row.examples)); + + return { + id: row.id, + title: row.title, + slug: row.slug, + description: row.description, + tags: deserializeJsonField(row.tags), + user, + colors: deserializeJsonField(row.colors), + examples, + published_at: row.published_at, + }; +} diff --git a/src/app/routes/health.ts b/src/app/routes/health.ts new file mode 100644 index 0000000..912bb45 --- /dev/null +++ b/src/app/routes/health.ts @@ -0,0 +1,8 @@ +import type { Hono } from "hono"; + +import type { AppBindings } from "../../config/types"; +import { getHealth } from "../controllers/health"; + +export function registerHealthRoutes(app: Hono): void { + app.get("/", getHealth); +} diff --git a/src/app/routes/lospec-palettes.ts b/src/app/routes/lospec-palettes.ts new file mode 100644 index 0000000..26ac878 --- /dev/null +++ b/src/app/routes/lospec-palettes.ts @@ -0,0 +1,8 @@ +import type { Hono } from "hono"; + +import type { AppBindings } from "../../config/types"; +import { getLospecPalettes } from "../controllers/lospec-palettes"; + +export function registerLospecPaletteRoutes(app: Hono): void { + app.get("/lospec_palettes", getLospecPalettes); +} diff --git a/src/app/services/lospec-api.ts b/src/app/services/lospec-api.ts new file mode 100644 index 0000000..71c86b1 --- /dev/null +++ b/src/app/services/lospec-api.ts @@ -0,0 +1,28 @@ +import { LOSPEC_PALETTE_LIST_URL } from "../../config/constants"; +import { + isLospecPaletteApiItem, + type LospecPaletteApiItem, +} from "../models/lospec-palette"; + +export async function fetchLospecPalettePage( + page: number, + signal: AbortSignal, +): Promise { + const url = new URL(LOSPEC_PALETTE_LIST_URL); + url.searchParams.set("colorNumberFilterType", "any"); + url.searchParams.set("page", String(page)); + url.searchParams.set("tag", ""); + url.searchParams.set("sortingType", "newest"); + + const response = await fetch(url, { signal }); + if (!response.ok) { + throw new Error(`Failed to fetch page ${page}: HTTP ${response.status}`); + } + + const payload = (await response.json()) as { palettes?: unknown }; + if (!Array.isArray(payload.palettes)) { + return []; + } + + return payload.palettes.filter(isLospecPaletteApiItem); +} diff --git a/src/app/services/lospec-palettes-repository.ts b/src/app/services/lospec-palettes-repository.ts new file mode 100644 index 0000000..e8f2b4e --- /dev/null +++ b/src/app/services/lospec-palettes-repository.ts @@ -0,0 +1,114 @@ +import { + mapPaletteToRow, + type ListLospecPalettesOptions, + type LospecPaletteApiItem, + type LospecPaletteRow, +} from "../models/lospec-palette"; +import { LOSPEC_PALETTES_PAGE_SIZE } from "../../config/constants"; + +export async function getExistingPaletteIds( + db: D1Database, + ids: string[], +): Promise> { + if (ids.length === 0) { + return new Set(); + } + + const placeholders = ids.map(() => "?").join(", "); + const statement = db.prepare( + `SELECT id FROM lospec_palettes WHERE id IN (${placeholders})`, + ); + const result = await statement.bind(...ids).all<{ id: string }>(); + + return new Set(result.results.map((row) => row.id)); +} + +export async function insertPalettes( + db: D1Database, + palettes: LospecPaletteApiItem[], +): Promise { + if (palettes.length === 0) { + return 0; + } + + const statements = palettes.map((palette) => { + const row = mapPaletteToRow(palette); + + return db + .prepare( + `INSERT OR IGNORE INTO lospec_palettes ( + id, + title, + slug, + description, + tags, + user, + colors, + examples, + published_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + row.id, + row.title, + row.slug, + row.description, + row.tags, + row.user, + row.colors, + row.examples, + row.published_at, + ); + }); + + await db.batch(statements); + return palettes.length; +} + +export async function listPalettes( + db: D1Database, + options: ListLospecPalettesOptions, +): Promise { + const whereClauses: string[] = []; + const bindings: Array = []; + + if (options.search) { + whereClauses.push("LOWER(COALESCE(title, '')) LIKE ?"); + bindings.push(`%${options.search.toLowerCase()}%`); + } + + if (options.tag) { + whereClauses.push(`EXISTS ( + SELECT 1 + FROM json_each(lospec_palettes.tags) + WHERE LOWER(CAST(json_each.value AS TEXT)) = ? + )`); + bindings.push(options.tag.toLowerCase()); + } + + const whereSql = + whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : ""; + const offset = options.page * LOSPEC_PALETTES_PAGE_SIZE; + + const result = await db + .prepare( + `SELECT + id, + title, + slug, + description, + tags, + user, + colors, + examples, + published_at + FROM lospec_palettes + ${whereSql} + ORDER BY published_at DESC, id DESC + LIMIT ? OFFSET ?`, + ) + .bind(...bindings, LOSPEC_PALETTES_PAGE_SIZE, offset) + .all(); + + return result.results; +} diff --git a/src/config/constants.ts b/src/config/constants.ts new file mode 100644 index 0000000..7563945 --- /dev/null +++ b/src/config/constants.ts @@ -0,0 +1,13 @@ +export const ALLOWED_ORIGINS = new Set([ + "https://2dtiler.com", + "https://app.2dtiler.com", + "http://localhost:4321", + "http://127.0.0.1:4321", + "http://localhost:8787", + "http://127.0.0.1:8787", +]); + +export const INTERNAL_API_KEY_HEADER = "X-Internal-Api-Key"; +export const LOSPEC_PALETTE_LIST_URL = "https://lospec.com/palette-list/load"; +export const LOSPEC_CDN_BASE_URL = "https://cdn.lospec.com/"; +export const LOSPEC_PALETTES_PAGE_SIZE = 100; diff --git a/src/config/types.ts b/src/config/types.ts new file mode 100644 index 0000000..389d5e0 --- /dev/null +++ b/src/config/types.ts @@ -0,0 +1,11 @@ +export interface RateLimiter { + limit(options: { key: string }): Promise<{ success: boolean }>; +} + +export interface Env { + DB: D1Database; + RATE_LIMITER: RateLimiter; + INTERNAL_API_KEY: string; +} + +export type AppBindings = { Bindings: Env }; diff --git a/src/index.ts b/src/index.ts index 4243b85..d8372a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,337 +1,6 @@ -import { Hono } from "hono"; - -interface RateLimiter { - limit(options: { key: string }): Promise<{ success: boolean }>; -} - -interface Env { - DB: D1Database; - RATE_LIMITER: RateLimiter; - INTERNAL_API_KEY: string; -} - -interface LospecPaletteApiItem { - _id: string; - title?: string; - slug?: string; - description?: string; - tags?: unknown; - user?: unknown; - colors?: unknown; - examples?: unknown; - publishedAt?: string; -} - -interface LospecPaletteRow { - id: string; - title: string | null; - slug: string | null; - description: string | null; - tags: string | null; - user: string | null; - colors: string | null; - examples: string | null; - published_at: string | null; -} - -interface LospecPaletteResponse { - id: string; - title: string | null; - slug: string | null; - description: string | null; - tags: unknown | null; - user: unknown | null; - colors: unknown | null; - examples: unknown | null; - published_at: string | null; -} - -const ALLOWED_ORIGINS = new Set([ - "https://2dtiler.com", - "https://app.2dtiler.com", - "http://localhost:4321", - "http://127.0.0.1:4321", -]); -const INTERNAL_API_KEY_HEADER = "X-Internal-Api-Key"; -const LOSPEC_PALETTE_LIST_URL = "https://lospec.com/palette-list/load"; - -const app = new Hono<{ Bindings: Env }>(); - -// ─── Helpers ──────────────────────────────────────────────────────────────── - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -function isLospecPaletteApiItem(value: unknown): value is LospecPaletteApiItem { - return isRecord(value) && typeof value._id === "string"; -} - -function asNullableString(value: unknown): string | null { - return typeof value === "string" ? value : null; -} - -function serializeJsonField(value: unknown): string | null { - if (value === undefined || value === null) { - return null; - } - - return JSON.stringify(value); -} - -function deserializeJsonField(value: string | null): unknown | null { - if (!value) { - return null; - } - - try { - return JSON.parse(value) as unknown; - } catch { - return value; - } -} - -function mapPaletteToRow(palette: LospecPaletteApiItem): LospecPaletteRow { - return { - id: palette._id, - title: asNullableString(palette.title), - slug: asNullableString(palette.slug), - description: asNullableString(palette.description), - tags: serializeJsonField(palette.tags), - user: serializeJsonField(palette.user), - colors: serializeJsonField(palette.colors), - examples: serializeJsonField(palette.examples), - published_at: asNullableString(palette.publishedAt), - }; -} - -function mapRowToResponse(row: LospecPaletteRow): LospecPaletteResponse { - return { - id: row.id, - title: row.title, - slug: row.slug, - description: row.description, - tags: deserializeJsonField(row.tags), - user: deserializeJsonField(row.user), - colors: deserializeJsonField(row.colors), - examples: deserializeJsonField(row.examples), - published_at: row.published_at, - }; -} - -async function fetchLospecPalettePage( - page: number, - signal: AbortSignal, -): Promise { - const url = new URL(LOSPEC_PALETTE_LIST_URL); - url.searchParams.set("colorNumberFilterType", "any"); - url.searchParams.set("page", String(page)); - url.searchParams.set("tag", ""); - url.searchParams.set("sortingType", "newest"); - - const response = await fetch(url, { signal }); - if (!response.ok) { - throw new Error(`Failed to fetch page ${page}: HTTP ${response.status}`); - } - - const payload = (await response.json()) as { palettes?: unknown }; - if (!Array.isArray(payload.palettes)) { - return []; - } - - return payload.palettes.filter(isLospecPaletteApiItem); -} - -async function getExistingPaletteIds( - db: D1Database, - ids: string[], -): Promise> { - if (ids.length === 0) { - return new Set(); - } - - const placeholders = ids.map(() => "?").join(", "); - const statement = db.prepare( - `SELECT id FROM lospec_palettes WHERE id IN (${placeholders})`, - ); - const result = await statement.bind(...ids).all<{ id: string }>(); - - return new Set(result.results.map((row) => row.id)); -} - -async function insertPalettes( - db: D1Database, - palettes: LospecPaletteApiItem[], -): Promise { - if (palettes.length === 0) { - return 0; - } - - const statements = palettes.map((palette) => { - const row = mapPaletteToRow(palette); - - return db - .prepare( - `INSERT OR IGNORE INTO lospec_palettes ( - id, - title, - slug, - description, - tags, - user, - colors, - examples, - published_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - ) - .bind( - row.id, - row.title, - row.slug, - row.description, - row.tags, - row.user, - row.colors, - row.examples, - row.published_at, - ); - }); - - await db.batch(statements); - return palettes.length; -} - -function applyCorsHeaders(headers: Headers, origin: string): void { - headers.set("Access-Control-Allow-Origin", origin); - headers.set("Access-Control-Allow-Methods", "GET, OPTIONS"); - headers.set("Access-Control-Allow-Headers", "Content-Type"); - headers.set("Vary", "Origin"); -} - -app.use("*", async (c, next) => { - const origin = c.req.header("Origin"); - const internalApiKey = c.req.header(INTERNAL_API_KEY_HEADER); - - if (c.env.INTERNAL_API_KEY && internalApiKey === c.env.INTERNAL_API_KEY) { - await next(); - return; - } - - if (!origin || !ALLOWED_ORIGINS.has(origin)) { - return c.json({ error: "Forbidden origin" }, 403); - } - - if (c.req.method === "OPTIONS") { - const headers = new Headers(); - applyCorsHeaders(headers, origin); - return new Response(null, { status: 204, headers }); - } - - await next(); - applyCorsHeaders(c.res.headers, origin); -}); - -// ─── HTTP Routes ───────────────────────────────────────────────────────────── - -/** - * GET / - * Returns a blank API route to verify the worker is running. - */ -app.get("/", async (c) => { - return c.json({}); -}); - -/** - * GET /lospec_palettes - * Returns all stored Lospec palette entries from D1. - */ -app.get("/lospec_palettes", async (c) => { - const ip = c.req.raw.headers.get("CF-Connecting-IP") ?? "unknown"; - const { success } = await c.env.RATE_LIMITER.limit({ key: ip }); - if (!success) { - return c.json({ error: "Rate limit exceeded. Try again in an hour." }, 429); - } - - const result = await c.env.DB.prepare( - `SELECT - id, - title, - slug, - description, - tags, - user, - colors, - examples, - published_at - FROM lospec_palettes - ORDER BY published_at DESC, id DESC`, - ).all(); - - return c.json(result.results.map(mapRowToResponse)); -}); - -// ─── Scheduled Job ─────────────────────────────────────────────────────────── - -/** - * Runs daily at 14:00 UTC (see wrangler.toml). - * - * Fetches Lospec palette pages from newest to oldest, inserts unseen rows into - * D1, and stops once a page contains any IDs that are already present. - */ -async function syncPalettes(env: Env, signal: AbortSignal): Promise { - let page = 0; - let pagesProcessed = 0; - let totalInserted = 0; - - while (!signal.aborted) { - try { - const palettes = await fetchLospecPalettePage(page, signal); - if (palettes.length === 0) { - console.log(`Page ${page}: no palettes returned; stopping pagination`); - break; - } - - const existingIds = await getExistingPaletteIds( - env.DB, - palettes.map((palette) => palette._id), - ); - const unseenPalettes = palettes.filter( - (palette) => !existingIds.has(palette._id), - ); - const inserted = await insertPalettes(env.DB, unseenPalettes); - - pagesProcessed++; - totalInserted += inserted; - - console.log( - `Page ${page}: fetched ${palettes.length}, inserted ${inserted}, existing ${existingIds.size}`, - ); - - if (existingIds.size > 0) { - console.log( - `Page ${page}: encountered existing palette IDs; stopping pagination`, - ); - break; - } - - page++; - } catch (error) { - if (signal.aborted) { - console.warn("Palette sync timed out; stopping early"); - break; - } - - console.error(`Page ${page}: failed to sync palettes`, error); - return; - } - } - - console.log( - `Palette sync complete: inserted ${totalInserted} palettes across ${pagesProcessed} page(s)`, - ); -} - -// ─── Worker Export ─────────────────────────────────────────────────────────── +import app from "./app"; +import { syncPalettes } from "./jobs/sync-palettes"; +import type { Env } from "./config/types"; export default { fetch: app.fetch, @@ -341,7 +10,7 @@ export default { env: Env, ctx: ExecutionContext, ): Promise { - const signal = AbortSignal.timeout(15 * 60 * 1000); // 15 minutes + const signal = AbortSignal.timeout(15 * 60 * 1000); ctx.waitUntil(syncPalettes(env, signal)); }, }; diff --git a/src/jobs/sync-palettes.ts b/src/jobs/sync-palettes.ts new file mode 100644 index 0000000..e068b69 --- /dev/null +++ b/src/jobs/sync-palettes.ts @@ -0,0 +1,62 @@ +import type { Env } from "../config/types"; +import { fetchLospecPalettePage } from "../app/services/lospec-api"; +import { + getExistingPaletteIds, + insertPalettes, +} from "../app/services/lospec-palettes-repository"; + +export async function syncPalettes( + env: Env, + signal: AbortSignal, +): Promise { + let page = 0; + let pagesProcessed = 0; + let totalInserted = 0; + + while (!signal.aborted) { + try { + const palettes = await fetchLospecPalettePage(page, signal); + if (palettes.length === 0) { + console.log(`Page ${page}: no palettes returned; stopping pagination`); + break; + } + + const existingIds = await getExistingPaletteIds( + env.DB, + palettes.map((palette) => palette._id), + ); + const unseenPalettes = palettes.filter( + (palette) => !existingIds.has(palette._id), + ); + const inserted = await insertPalettes(env.DB, unseenPalettes); + + pagesProcessed++; + totalInserted += inserted; + + console.log( + `Page ${page}: fetched ${palettes.length}, inserted ${inserted}, existing ${existingIds.size}`, + ); + + if (existingIds.size > 0) { + console.log( + `Page ${page}: encountered existing palette IDs; stopping pagination`, + ); + break; + } + + page++; + } catch (error) { + if (signal.aborted) { + console.warn("Palette sync timed out; stopping early"); + break; + } + + console.error(`Page ${page}: failed to sync palettes`, error); + return; + } + } + + console.log( + `Palette sync complete: inserted ${totalInserted} palettes across ${pagesProcessed} page(s)`, + ); +} diff --git a/tests/integration/.gitkeep b/tests/integration/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json index b7eb1cb..0cda447 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,5 +9,5 @@ "noEmit": true, "skipLibCheck": true }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "tests/**/*.ts"] } \ No newline at end of file From 7950e7ac89111ddb6a3f90c3d8bf328500dbc31d Mon Sep 17 00:00:00 2001 From: Werner Bihl Date: Fri, 10 Apr 2026 14:29:32 +0200 Subject: [PATCH 3/7] Add deployment workflow for development environment and update TODO list --- .github/workflows/test-dev.yml | 28 ++++++++++++++++++++++++++++ TODO.txt | 6 ++---- tests/integration/.gitkeep | 0 tests/unit/.gitkeep | 0 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/test-dev.yml delete mode 100644 tests/integration/.gitkeep delete mode 100644 tests/unit/.gitkeep diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml new file mode 100644 index 0000000..bd7500a --- /dev/null +++ b/.github/workflows/test-dev.yml @@ -0,0 +1,28 @@ +name: Deploy Dev + +on: + pull_request: + branches: + - main + # Allows you to run this workflow manually from the Actions tab on GitHub. + workflow_dispatch: + +# Allow this job to clone the repo and create a page deployment +permissions: + contents: read + pages: write + id-token: write + +jobs: + test-api: + name: Test API + runs-on: ubuntu-latest + steps: + - name: Checkout your repository using git + uses: actions/checkout@v6.0.2 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2.2.0 + + - name: Install dependencies + run: bun install diff --git a/TODO.txt b/TODO.txt index 47873a9..9eadbc5 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,5 +1,3 @@ -Our endpoint should also accept sorting and pagination - -Create API endpoint for fetching releases Add unit tests and add to CI/CD -Update README.md \ No newline at end of file +Update README.md +Push and deploy \ No newline at end of file diff --git a/tests/integration/.gitkeep b/tests/integration/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep deleted file mode 100644 index e69de29..0000000 From 143db03fa69be502527b9409d334b7d66bb38f23 Mon Sep 17 00:00:00 2001 From: Werner Bihl Date: Fri, 10 Apr 2026 15:00:58 +0200 Subject: [PATCH 4/7] Add tests for app functionality, models, services, and syncing palettes - Implement tests for the main app routes, including CORS handling and error responses. - Create unit tests for lospec palette models and services, ensuring correct mapping and fetching behavior. - Add tests for the syncPalettes job to verify the syncing logic and error handling. - Introduce a test environment setup to facilitate database interactions in tests. - Configure Vitest for testing with coverage reporting. - Generate initial coverage report for the application. --- .github/workflows/test-dev.yml | 3 + bun.lock | 247 +++++++- coverage/lcov-report/base.css | 224 +++++++ coverage/lcov-report/block-navigation.js | 87 +++ coverage/lcov-report/favicon.png | Bin 0 -> 445 bytes coverage/lcov-report/index.html | 221 +++++++ coverage/lcov-report/prettify.css | 1 + coverage/lcov-report/prettify.js | 2 + coverage/lcov-report/sort-arrow-sprite.png | Bin 0 -> 138 bytes coverage/lcov-report/sorter.js | 210 +++++++ coverage/lcov-report/src/app.ts.html | 130 ++++ .../src/app/controllers/health.ts.html | 106 ++++ .../src/app/controllers/index.html | 131 +++++ .../app/controllers/lospec-palettes.ts.html | 277 +++++++++ .../src/app/middleware/cors.ts.html | 220 +++++++ .../lcov-report/src/app/middleware/index.html | 116 ++++ .../lcov-report/src/app/models/index.html | 116 ++++ .../src/app/models/lospec-palette.ts.html | 556 ++++++++++++++++++ .../lcov-report/src/app/routes/health.ts.html | 109 ++++ .../lcov-report/src/app/routes/index.html | 131 +++++ .../src/app/routes/lospec-palettes.ts.html | 109 ++++ .../lcov-report/src/app/services/index.html | 131 +++++ .../src/app/services/lospec-api.ts.html | 169 ++++++ .../lospec-palettes-repository.ts.html | 427 ++++++++++++++ .../lcov-report/src/config/constants.ts.html | 124 ++++ coverage/lcov-report/src/config/index.html | 116 ++++ coverage/lcov-report/src/index.html | 131 +++++ coverage/lcov-report/src/index.ts.html | 133 +++++ coverage/lcov-report/src/jobs/index.html | 116 ++++ .../src/jobs/sync-palettes.ts.html | 271 +++++++++ coverage/lcov.info | 385 ++++++++++++ package.json | 6 +- tests/app.test.ts | 196 ++++++ tests/helpers/env.ts | 31 + tests/models-and-services.test.ts | 212 +++++++ tests/sync-palettes.test.ts | 110 ++++ tests/worker.test.ts | 37 ++ vitest.config.ts | 28 + 38 files changed, 5615 insertions(+), 4 deletions(-) create mode 100644 coverage/lcov-report/base.css create mode 100644 coverage/lcov-report/block-navigation.js create mode 100644 coverage/lcov-report/favicon.png create mode 100644 coverage/lcov-report/index.html create mode 100644 coverage/lcov-report/prettify.css create mode 100644 coverage/lcov-report/prettify.js create mode 100644 coverage/lcov-report/sort-arrow-sprite.png create mode 100644 coverage/lcov-report/sorter.js create mode 100644 coverage/lcov-report/src/app.ts.html create mode 100644 coverage/lcov-report/src/app/controllers/health.ts.html create mode 100644 coverage/lcov-report/src/app/controllers/index.html create mode 100644 coverage/lcov-report/src/app/controllers/lospec-palettes.ts.html create mode 100644 coverage/lcov-report/src/app/middleware/cors.ts.html create mode 100644 coverage/lcov-report/src/app/middleware/index.html create mode 100644 coverage/lcov-report/src/app/models/index.html create mode 100644 coverage/lcov-report/src/app/models/lospec-palette.ts.html create mode 100644 coverage/lcov-report/src/app/routes/health.ts.html create mode 100644 coverage/lcov-report/src/app/routes/index.html create mode 100644 coverage/lcov-report/src/app/routes/lospec-palettes.ts.html create mode 100644 coverage/lcov-report/src/app/services/index.html create mode 100644 coverage/lcov-report/src/app/services/lospec-api.ts.html create mode 100644 coverage/lcov-report/src/app/services/lospec-palettes-repository.ts.html create mode 100644 coverage/lcov-report/src/config/constants.ts.html create mode 100644 coverage/lcov-report/src/config/index.html create mode 100644 coverage/lcov-report/src/index.html create mode 100644 coverage/lcov-report/src/index.ts.html create mode 100644 coverage/lcov-report/src/jobs/index.html create mode 100644 coverage/lcov-report/src/jobs/sync-palettes.ts.html create mode 100644 coverage/lcov.info create mode 100644 tests/app.test.ts create mode 100644 tests/helpers/env.ts create mode 100644 tests/models-and-services.test.ts create mode 100644 tests/sync-palettes.test.ts create mode 100644 tests/worker.test.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index bd7500a..8e4f658 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -26,3 +26,6 @@ jobs: - name: Install dependencies run: bun install + + - name: Run tests with coverage gate + run: bun run test diff --git a/bun.lock b/bun.lock index 50a835f..6dfa657 100644 --- a/bun.lock +++ b/bun.lock @@ -8,20 +8,59 @@ "hono": "^4.12.12", }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.14.3", "@cloudflare/workers-types": "^4.20260410.1", "@eslint/js": "^10.0.1", + "@vitest/coverage-istanbul": "^4.1.4", "eslint": "^10.2.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.1", + "vitest": "^4.1.4", "wrangler": "^4.81.1", }, }, }, "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg=="], + "@cloudflare/vitest-pool-workers": ["@cloudflare/vitest-pool-workers@0.14.3", "", { "dependencies": { "cjs-module-lexer": "^1.2.3", "esbuild": "0.27.3", "miniflare": "4.20260409.0", "wrangler": "4.81.1", "zod": "^3.25.76" }, "peerDependencies": { "@vitest/runner": "^4.1.0", "@vitest/snapshot": "^4.1.0", "vitest": "^4.1.0" } }, "sha512-7J0K3f9iS2u6k2J/bY7/vJJcaLgEGXcfNrs2fSti6vc0l/L/I4XmYtvZ1JwmFa5xqiHG4tF0ktGSKUZbkqvzEw=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260409.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-h/bkaC0HJL63aqAGnV0oagqpBiTSstabODThkeMSbG8kctl0Jb4jlq1pNHJPmYGazFNtfyagrUZFb6HN22GX7w=="], "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260409.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HTAC+B9uSYcm+GjN3UYJjuun19GqYtK1bAFJ0KECXyfsgIDwH1MTzxbTxzJpZUbWLw8s0jcwCU06MWZj6cgnxQ=="], @@ -36,8 +75,12 @@ "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], @@ -164,11 +207,21 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], + + "@oxc-project/types": ["@oxc-project/types@0.124.0", "", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="], "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], @@ -176,10 +229,50 @@ "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.15", "", { "os": "freebsd", "cpu": "x64" }, "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm" }, "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "ppc64" }, "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "s390x" }, "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "x64" }, "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.15", "", { "os": "linux", "cpu": "x64" }, "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.15", "", { "os": "none", "cpu": "arm64" }, "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.15", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.3" }, "cpu": "none" }, "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.15", "", { "os": "win32", "cpu": "x64" }, "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="], + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -206,18 +299,52 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], + "@vitest/coverage-istanbul": ["@vitest/coverage-istanbul@4.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@istanbuljs/schema": "^0.1.3", "@jridgewell/gen-mapping": "^0.3.13", "@jridgewell/trace-mapping": "0.3.31", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "vitest": "4.1.4" } }, "sha512-Pyi4F8RnqU6hBGiIDhS/e8gVD4FRcUvZJ2AbFiIlmIxHlEIsKyCxGOqufCECobty/dXELcN8oIH4Gms3hVOCYA=="], + + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.4", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.4", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.4", "vitest": "4.1.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w=="], + + "@vitest/expect": ["@vitest/expect@4.1.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.4", "", { "dependencies": { "@vitest/spy": "4.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.4", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A=="], + + "@vitest/runner": ["@vitest/runner@4.1.4", "", { "dependencies": { "@vitest/utils": "4.1.4", "pathe": "^2.0.3" } }, "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.4", "", { "dependencies": { "@vitest/pretty-format": "4.1.4", "@vitest/utils": "4.1.4", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw=="], + + "@vitest/spy": ["@vitest/spy@4.1.4", "", {}, "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ=="], + + "@vitest/utils": ["@vitest/utils@4.1.4", "", { "dependencies": { "@vitest/pretty-format": "4.1.4", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg=="], + "balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001787", "", {}, "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -228,10 +355,16 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.334", "", {}, "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog=="], + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.4", "@eslint/config-helpers": "^0.5.4", "@eslint/core": "^1.2.0", "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA=="], @@ -248,8 +381,12 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -268,10 +405,16 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "hono": ["hono@4.12.12", "", {}, "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q=="], + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -282,28 +425,78 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + "miniflare": ["miniflare@4.20260409.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.4", "workerd": "1.20260409.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-ayl6To4av0YuXsSivGgWLj+Ug8xZ0Qz3sGV8+Ok2LhNVl6m8m5ktEBM3LX9iT9MtLZRJwBlJrKcraNs/DlZQfA=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -318,13 +511,19 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "rolldown": ["rolldown@1.0.0-rc.15", "", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], @@ -332,10 +531,24 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -350,10 +563,18 @@ "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "vite": ["vite@8.0.8", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="], + + "vitest": ["vitest@4.1.4", "", { "dependencies": { "@vitest/expect": "4.1.4", "@vitest/mocker": "4.1.4", "@vitest/pretty-format": "4.1.4", "@vitest/runner": "4.1.4", "@vitest/snapshot": "4.1.4", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.4", "@vitest/browser-preview": "4.1.4", "@vitest/browser-webdriverio": "4.1.4", "@vitest/coverage-istanbul": "4.1.4", "@vitest/coverage-v8": "4.1.4", "@vitest/ui": "4.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "workerd": ["workerd@1.20260409.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260409.1", "@cloudflare/workerd-darwin-arm64": "1.20260409.1", "@cloudflare/workerd-linux-64": "1.20260409.1", "@cloudflare/workerd-linux-arm64": "1.20260409.1", "@cloudflare/workerd-windows-64": "1.20260409.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kuWP20fAaqaLBqLbvUfY9nCF6c3C78L60G9lS6eVwBf+v8trVFIsAdLB/FtrnKm7vgVvpDzvFAfB80VIiVj95w=="], @@ -362,16 +583,36 @@ "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.0", "", {}, "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q=="], + + "ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + + "make-dir/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], } } diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 96.02% + Statements + 145/151 +
+ + +
+ 86.36% + Branches + 57/66 +
+ + +
+ 100% + Functions + 31/31 +
+ + +
+ 95.97% + Lines + 143/149 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
src +
+
100%6/6100%0/0100%1/1100%6/6
src/app/controllers +
+
100%21/21100%14/14100%5/5100%21/21
src/app/middleware +
+
100%21/21100%10/10100%2/2100%21/21
src/app/models +
+
80%24/3072.72%16/22100%11/1180%24/30
src/app/routes +
+
100%2/2100%0/0100%2/2100%2/2
src/app/services +
+
100%39/3978.57%11/14100%7/7100%37/37
src/config +
+
100%5/5100%0/0100%0/0100%5/5
src/jobs +
+
100%27/27100%6/6100%3/3100%27/27
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/lcov-report/src/app.ts.html b/coverage/lcov-report/src/app.ts.html new file mode 100644 index 0000000..9d623fd --- /dev/null +++ b/coverage/lcov-report/src/app.ts.html @@ -0,0 +1,130 @@ + + + + + + Code coverage report for src/app.ts + + + + + + + + + +
+
+

All files / src app.ts

+
+ +
+ 100% + Statements + 4/4 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 4/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16  +  +  +  +  +  +  +2x +  +2x +  +2x +2x +  +  + 
import { Hono } from "hono";
+ 
+import { registerHealthRoutes } from "./app/routes/health";
+import { registerLospecPaletteRoutes } from "./app/routes/lospec-palettes";
+import { corsMiddleware } from "./app/middleware/cors";
+import type { AppBindings } from "./config/types";
+ 
+const app = new Hono<AppBindings>();
+ 
+app.use("*", corsMiddleware);
+ 
+registerHealthRoutes(app);
+registerLospecPaletteRoutes(app);
+ 
+export default app;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/controllers/health.ts.html b/coverage/lcov-report/src/app/controllers/health.ts.html new file mode 100644 index 0000000..3c5418f --- /dev/null +++ b/coverage/lcov-report/src/app/controllers/health.ts.html @@ -0,0 +1,106 @@ + + + + + + Code coverage report for src/app/controllers/health.ts + + + + + + + + + +
+
+

All files / src/app/controllers health.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8  +  +  +  +  +3x +  + 
import type { Context } from "hono";
+ 
+import type { AppBindings } from "../../config/types";
+ 
+export function getHealth(c: Context<AppBindings>): Response {
+  return c.json({});
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/controllers/index.html b/coverage/lcov-report/src/app/controllers/index.html new file mode 100644 index 0000000..0632fde --- /dev/null +++ b/coverage/lcov-report/src/app/controllers/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for src/app/controllers + + + + + + + + + +
+
+

All files src/app/controllers

+
+ +
+ 100% + Statements + 21/21 +
+ + +
+ 100% + Branches + 14/14 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 100% + Lines + 21/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
health.ts +
+
100%1/1100%0/0100%1/1100%1/1
lospec-palettes.ts +
+
100%20/20100%14/14100%4/4100%20/20
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/controllers/lospec-palettes.ts.html b/coverage/lcov-report/src/app/controllers/lospec-palettes.ts.html new file mode 100644 index 0000000..da8f6ed --- /dev/null +++ b/coverage/lcov-report/src/app/controllers/lospec-palettes.ts.html @@ -0,0 +1,277 @@ + + + + + + Code coverage report for src/app/controllers/lospec-palettes.ts + + + + + + + + + +
+
+

All files / src/app/controllers lospec-palettes.ts

+
+ +
+ 100% + Statements + 20/20 +
+ + +
+ 100% + Branches + 14/14 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 20/20 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65  +  +  +  +  +  +  +  +  +  +  +3x +1x +  +  +2x +1x +  +  +1x +  +  +  +4x +4x +  +  +  +  +  +3x +3x +1x +  +  +2x +  +  +  +  +  +  +  +  +  +3x +3x +1x +  +  +  +  +  +  +  +2x +2x +2x +1x +  +  +1x +1x +  + 
import type { Context } from "hono";
+ 
+import { LOSPEC_PALETTES_PAGE_SIZE } from "../../config/constants";
+import type { AppBindings } from "../../config/types";
+import {
+  mapRowToResponse,
+  type ListLospecPalettesOptions,
+} from "../models/lospec-palette";
+import { listPalettes } from "../services/lospec-palettes-repository";
+ 
+function parsePage(value: string | undefined): number | null {
+  if (value === undefined) {
+    return 0;
+  }
+ 
+  if (!/^\d+$/.test(value)) {
+    return null;
+  }
+ 
+  return Number.parseInt(value, 10);
+}
+ 
+function normalizeQueryValue(value: string | undefined): string | undefined {
+  const trimmed = value?.trim();
+  return trimmed ? trimmed : undefined;
+}
+ 
+function parseListLospecPalettesOptions(
+  query: Record<string, string | undefined>,
+): ListLospecPalettesOptions | null {
+  const page = parsePage(query.page);
+  if (page === null) {
+    return null;
+  }
+ 
+  return {
+    page,
+    search: normalizeQueryValue(query.search),
+    tag: normalizeQueryValue(query.tags),
+  };
+}
+ 
+export async function getLospecPalettes(
+  c: Context<AppBindings>,
+): Promise<Response> {
+  const options = parseListLospecPalettesOptions(c.req.query());
+  if (!options) {
+    return c.json(
+      {
+        error: `Invalid page parameter. Expected a non-negative integer with ${LOSPEC_PALETTES_PAGE_SIZE} results per page.`,
+      },
+      400,
+    );
+  }
+ 
+  const ip = c.req.raw.headers.get("CF-Connecting-IP") ?? "unknown";
+  const { success } = await c.env.RATE_LIMITER.limit({ key: ip });
+  if (!success) {
+    return c.json({ error: "Rate limit exceeded. Try again in an hour." }, 429);
+  }
+ 
+  const palettes = await listPalettes(c.env.DB, options);
+  return c.json(palettes.map(mapRowToResponse));
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/middleware/cors.ts.html b/coverage/lcov-report/src/app/middleware/cors.ts.html new file mode 100644 index 0000000..2ab29a5 --- /dev/null +++ b/coverage/lcov-report/src/app/middleware/cors.ts.html @@ -0,0 +1,220 @@ + + + + + + Code coverage report for src/app/middleware/cors.ts + + + + + + + + + +
+
+

All files / src/app/middleware cors.ts

+
+ +
+ 100% + Statements + 21/21 +
+ + +
+ 100% + Branches + 10/10 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 21/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46  +  +  +  +  +  +  +  +  +2x +2x +2x +2x +  +  +2x +  +  +  +8x +8x +  +8x +1x +1x +  +  +7x +4x +4x +  +  +3x +1x +  +  +2x +1x +1x +1x +  +  +1x +1x +  + 
import type { MiddlewareHandler } from "hono";
+ 
+import {
+  ALLOWED_ORIGINS,
+  INTERNAL_API_KEY_HEADER,
+} from "../../config/constants";
+import type { AppBindings } from "../../config/types";
+ 
+function applyCorsHeaders(headers: Headers, origin: string): void {
+  headers.set("Access-Control-Allow-Origin", origin);
+  headers.set("Access-Control-Allow-Methods", "GET, OPTIONS");
+  headers.set("Access-Control-Allow-Headers", "Content-Type");
+  headers.set("Vary", "Origin");
+}
+ 
+export const corsMiddleware: MiddlewareHandler<AppBindings> = async (
+  c,
+  next,
+) => {
+  const origin = c.req.header("Origin");
+  const internalApiKey = c.req.header(INTERNAL_API_KEY_HEADER);
+ 
+  if (c.env.INTERNAL_API_KEY && internalApiKey === c.env.INTERNAL_API_KEY) {
+    await next();
+    return;
+  }
+ 
+  if (!origin) {
+    await next();
+    return;
+  }
+ 
+  if (!ALLOWED_ORIGINS.has(origin)) {
+    return c.json({ error: "Forbidden origin" }, 403);
+  }
+ 
+  if (c.req.method === "OPTIONS") {
+    const headers = new Headers();
+    applyCorsHeaders(headers, origin);
+    return new Response(null, { status: 204, headers });
+  }
+ 
+  await next();
+  applyCorsHeaders(c.res.headers, origin);
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/middleware/index.html b/coverage/lcov-report/src/app/middleware/index.html new file mode 100644 index 0000000..4df88a3 --- /dev/null +++ b/coverage/lcov-report/src/app/middleware/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for src/app/middleware + + + + + + + + + +
+
+

All files src/app/middleware

+
+ +
+ 100% + Statements + 21/21 +
+ + +
+ 100% + Branches + 10/10 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 21/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cors.ts +
+
100%21/21100%10/10100%2/2100%21/21
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/models/index.html b/coverage/lcov-report/src/app/models/index.html new file mode 100644 index 0000000..ee6eeca --- /dev/null +++ b/coverage/lcov-report/src/app/models/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for src/app/models + + + + + + + + + +
+
+

All files src/app/models

+
+ +
+ 80% + Statements + 24/30 +
+ + +
+ 72.72% + Branches + 16/22 +
+ + +
+ 100% + Functions + 11/11 +
+ + +
+ 80% + Lines + 24/30 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
lospec-palette.ts +
+
80%24/3072.72%16/22100%11/1180%24/30
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/models/lospec-palette.ts.html b/coverage/lcov-report/src/app/models/lospec-palette.ts.html new file mode 100644 index 0000000..0feadc2 --- /dev/null +++ b/coverage/lcov-report/src/app/models/lospec-palette.ts.html @@ -0,0 +1,556 @@ + + + + + + Code coverage report for src/app/models/lospec-palette.ts + + + + + + + + + +
+
+

All files / src/app/models lospec-palette.ts

+
+ +
+ 80% + Statements + 24/30 +
+ + +
+ 72.72% + Branches + 16/22 +
+ + +
+ 100% + Functions + 11/11 +
+ + +
+ 80% + Lines + 24/30 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +11x +  +  +  +  +  +4x +  +  +  +14x +  +  +  +3x +  +  +  +3x +  +  +  +3x +  +  +  +3x +  +  +  +  +  +3x +  +  +  +3x +4x +  +  +  +4x +4x +1x +  +  +3x +  +  +  +  +  +  +  +  +  +3x +  +  +  +3x +  +  +  +8x +  +  +  +8x +8x +  +1x +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +  +2x +  +  +  +  +  +  +  +  +  +  +  + 
import { LOSPEC_CDN_BASE_URL } from "../../config/constants";
+ 
+export interface LospecPaletteApiItem {
+  _id: string;
+  title?: string;
+  slug?: string;
+  description?: string;
+  tags?: unknown;
+  user?: unknown;
+  colors?: unknown;
+  examples?: unknown;
+  publishedAt?: string;
+}
+ 
+export interface LospecPaletteRow {
+  id: string;
+  title: string | null;
+  slug: string | null;
+  description: string | null;
+  tags: string | null;
+  user: string | null;
+  colors: string | null;
+  examples: string | null;
+  published_at: string | null;
+}
+ 
+export interface LospecPaletteExample {
+  image: string;
+  description: string | null;
+}
+ 
+export interface LospecPaletteResponse {
+  id: string;
+  title: string | null;
+  slug: string | null;
+  description: string | null;
+  tags: unknown | null;
+  user: string | null;
+  colors: unknown | null;
+  examples: LospecPaletteExample[] | null;
+  published_at: string | null;
+}
+ 
+export interface ListLospecPalettesOptions {
+  page: number;
+  search?: string;
+  tag?: string;
+}
+ 
+function isRecord(value: unknown): value is Record<string, unknown> {
+  return typeof value === "object" && value !== null;
+}
+ 
+export function isLospecPaletteApiItem(
+  value: unknown,
+): value is LospecPaletteApiItem {
+  return isRecord(value) && typeof value._id === "string";
+}
+ 
+function asNullableString(value: unknown): string | null {
+  return typeof value === "string" ? value : null;
+}
+ 
+function normalizePaletteUser(value: unknown): string | null {
+  Iif (typeof value === "string") {
+    return value;
+  }
+ 
+  Iif (!isRecord(value)) {
+    return null;
+  }
+ 
+  return asNullableString(value.name);
+}
+ 
+function normalizePaletteExampleImage(value: string): string {
+  return new URL(value, LOSPEC_CDN_BASE_URL).toString();
+}
+ 
+function normalizePaletteExamples(
+  value: unknown,
+): LospecPaletteExample[] | null {
+  Iif (!Array.isArray(value)) {
+    return null;
+  }
+ 
+  return value.flatMap((example) => {
+    Iif (!isRecord(example)) {
+      return [];
+    }
+ 
+    const image = asNullableString(example.image);
+    if (!image) {
+      return [];
+    }
+ 
+    return [
+      {
+        image: normalizePaletteExampleImage(image),
+        description: asNullableString(example.description),
+      },
+    ];
+  });
+}
+ 
+function serializeJsonField(value: unknown): string | null {
+  Iif (value === undefined || value === null) {
+    return null;
+  }
+ 
+  return JSON.stringify(value);
+}
+ 
+function deserializeJsonField(value: string | null): unknown | null {
+  Iif (!value) {
+    return null;
+  }
+ 
+  try {
+    return JSON.parse(value) as unknown;
+  } catch {
+    return value;
+  }
+}
+ 
+export function mapPaletteToRow(
+  palette: LospecPaletteApiItem,
+): LospecPaletteRow {
+  return {
+    id: palette._id,
+    title: asNullableString(palette.title),
+    slug: asNullableString(palette.slug),
+    description: asNullableString(palette.description),
+    tags: serializeJsonField(palette.tags),
+    user: normalizePaletteUser(palette.user),
+    colors: serializeJsonField(palette.colors),
+    examples: serializeJsonField(normalizePaletteExamples(palette.examples)),
+    published_at: asNullableString(palette.publishedAt),
+  };
+}
+ 
+export function mapRowToResponse(row: LospecPaletteRow): LospecPaletteResponse {
+  const user = normalizePaletteUser(deserializeJsonField(row.user));
+  const examples = normalizePaletteExamples(deserializeJsonField(row.examples));
+ 
+  return {
+    id: row.id,
+    title: row.title,
+    slug: row.slug,
+    description: row.description,
+    tags: deserializeJsonField(row.tags),
+    user,
+    colors: deserializeJsonField(row.colors),
+    examples,
+    published_at: row.published_at,
+  };
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/routes/health.ts.html b/coverage/lcov-report/src/app/routes/health.ts.html new file mode 100644 index 0000000..dcb99c2 --- /dev/null +++ b/coverage/lcov-report/src/app/routes/health.ts.html @@ -0,0 +1,109 @@ + + + + + + Code coverage report for src/app/routes/health.ts + + + + + + + + + +
+
+

All files / src/app/routes health.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9  +  +  +  +  +  +2x +  + 
import type { Hono } from "hono";
+ 
+import type { AppBindings } from "../../config/types";
+import { getHealth } from "../controllers/health";
+ 
+export function registerHealthRoutes(app: Hono<AppBindings>): void {
+  app.get("/", getHealth);
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/routes/index.html b/coverage/lcov-report/src/app/routes/index.html new file mode 100644 index 0000000..e8e6c98 --- /dev/null +++ b/coverage/lcov-report/src/app/routes/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for src/app/routes + + + + + + + + + +
+
+

All files src/app/routes

+
+ +
+ 100% + Statements + 2/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 2/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
health.ts +
+
100%1/1100%0/0100%1/1100%1/1
lospec-palettes.ts +
+
100%1/1100%0/0100%1/1100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/routes/lospec-palettes.ts.html b/coverage/lcov-report/src/app/routes/lospec-palettes.ts.html new file mode 100644 index 0000000..bdf8093 --- /dev/null +++ b/coverage/lcov-report/src/app/routes/lospec-palettes.ts.html @@ -0,0 +1,109 @@ + + + + + + Code coverage report for src/app/routes/lospec-palettes.ts + + + + + + + + + +
+
+

All files / src/app/routes lospec-palettes.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9  +  +  +  +  +  +2x +  + 
import type { Hono } from "hono";
+ 
+import type { AppBindings } from "../../config/types";
+import { getLospecPalettes } from "../controllers/lospec-palettes";
+ 
+export function registerLospecPaletteRoutes(app: Hono<AppBindings>): void {
+  app.get("/lospec_palettes", getLospecPalettes);
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/services/index.html b/coverage/lcov-report/src/app/services/index.html new file mode 100644 index 0000000..5127123 --- /dev/null +++ b/coverage/lcov-report/src/app/services/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for src/app/services + + + + + + + + + +
+
+

All files src/app/services

+
+ +
+ 100% + Statements + 39/39 +
+ + +
+ 78.57% + Branches + 11/14 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 100% + Lines + 37/37 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
lospec-api.ts +
+
100%12/12100%4/4100%1/1100%12/12
lospec-palettes-repository.ts +
+
100%27/2770%7/10100%6/6100%25/25
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/services/lospec-api.ts.html b/coverage/lcov-report/src/app/services/lospec-api.ts.html new file mode 100644 index 0000000..91592dc --- /dev/null +++ b/coverage/lcov-report/src/app/services/lospec-api.ts.html @@ -0,0 +1,169 @@ + + + + + + Code coverage report for src/app/services/lospec-api.ts + + + + + + + + + +
+
+

All files / src/app/services lospec-api.ts

+
+ +
+ 100% + Statements + 12/12 +
+ + +
+ 100% + Branches + 4/4 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 12/12 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29  +  +  +  +  +  +  +  +  +  +3x +3x +3x +3x +3x +  +3x +3x +1x +  +  +2x +2x +1x +  +  +1x +  + 
import { LOSPEC_PALETTE_LIST_URL } from "../../config/constants";
+import {
+  isLospecPaletteApiItem,
+  type LospecPaletteApiItem,
+} from "../models/lospec-palette";
+ 
+export async function fetchLospecPalettePage(
+  page: number,
+  signal: AbortSignal,
+): Promise<LospecPaletteApiItem[]> {
+  const url = new URL(LOSPEC_PALETTE_LIST_URL);
+  url.searchParams.set("colorNumberFilterType", "any");
+  url.searchParams.set("page", String(page));
+  url.searchParams.set("tag", "");
+  url.searchParams.set("sortingType", "newest");
+ 
+  const response = await fetch(url, { signal });
+  if (!response.ok) {
+    throw new Error(`Failed to fetch page ${page}: HTTP ${response.status}`);
+  }
+ 
+  const payload = (await response.json()) as { palettes?: unknown };
+  if (!Array.isArray(payload.palettes)) {
+    return [];
+  }
+ 
+  return payload.palettes.filter(isLospecPaletteApiItem);
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/app/services/lospec-palettes-repository.ts.html b/coverage/lcov-report/src/app/services/lospec-palettes-repository.ts.html new file mode 100644 index 0000000..e3ed9eb --- /dev/null +++ b/coverage/lcov-report/src/app/services/lospec-palettes-repository.ts.html @@ -0,0 +1,427 @@ + + + + + + Code coverage report for src/app/services/lospec-palettes-repository.ts + + + + + + + + + +
+
+

All files / src/app/services lospec-palettes-repository.ts

+
+ +
+ 100% + Statements + 27/27 +
+ + +
+ 70% + Branches + 7/10 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 25/25 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115  +  +  +  +  +  +  +  +  +  +  +  +2x +1x +  +  +2x +1x +  +  +1x +  +2x +  +  +  +  +  +  +2x +1x +  +  +1x +1x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +1x +  +1x +1x +1x +  +  +1x +1x +  +  +  +  +1x +  +  +  +1x +1x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  + 
import {
+  mapPaletteToRow,
+  type ListLospecPalettesOptions,
+  type LospecPaletteApiItem,
+  type LospecPaletteRow,
+} from "../models/lospec-palette";
+import { LOSPEC_PALETTES_PAGE_SIZE } from "../../config/constants";
+ 
+export async function getExistingPaletteIds(
+  db: D1Database,
+  ids: string[],
+): Promise<Set<string>> {
+  if (ids.length === 0) {
+    return new Set();
+  }
+ 
+  const placeholders = ids.map(() => "?").join(", ");
+  const statement = db.prepare(
+    `SELECT id FROM lospec_palettes WHERE id IN (${placeholders})`,
+  );
+  const result = await statement.bind(...ids).all<{ id: string }>();
+ 
+  return new Set(result.results.map((row) => row.id));
+}
+ 
+export async function insertPalettes(
+  db: D1Database,
+  palettes: LospecPaletteApiItem[],
+): Promise<number> {
+  if (palettes.length === 0) {
+    return 0;
+  }
+ 
+  const statements = palettes.map((palette) => {
+    const row = mapPaletteToRow(palette);
+ 
+    return db
+      .prepare(
+        `INSERT OR IGNORE INTO lospec_palettes (
+          id,
+          title,
+          slug,
+          description,
+          tags,
+          user,
+          colors,
+          examples,
+          published_at
+        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+      )
+      .bind(
+        row.id,
+        row.title,
+        row.slug,
+        row.description,
+        row.tags,
+        row.user,
+        row.colors,
+        row.examples,
+        row.published_at,
+      );
+  });
+ 
+  await db.batch(statements);
+  return palettes.length;
+}
+ 
+export async function listPalettes(
+  db: D1Database,
+  options: ListLospecPalettesOptions,
+): Promise<LospecPaletteRow[]> {
+  const whereClauses: string[] = [];
+  const bindings: Array<number | string> = [];
+ 
+  Eif (options.search) {
+    whereClauses.push("LOWER(COALESCE(title, '')) LIKE ?");
+    bindings.push(`%${options.search.toLowerCase()}%`);
+  }
+ 
+  Eif (options.tag) {
+    whereClauses.push(`EXISTS (
+      SELECT 1
+      FROM json_each(lospec_palettes.tags)
+      WHERE LOWER(CAST(json_each.value AS TEXT)) = ?
+    )`);
+    bindings.push(options.tag.toLowerCase());
+  }
+ 
+  const whereSql =
+    whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
+  const offset = options.page * LOSPEC_PALETTES_PAGE_SIZE;
+ 
+  const result = await db
+    .prepare(
+      `SELECT
+        id,
+        title,
+        slug,
+        description,
+        tags,
+        user,
+        colors,
+        examples,
+        published_at
+      FROM lospec_palettes
+      ${whereSql}
+      ORDER BY published_at DESC, id DESC
+      LIMIT ? OFFSET ?`,
+    )
+    .bind(...bindings, LOSPEC_PALETTES_PAGE_SIZE, offset)
+    .all<LospecPaletteRow>();
+ 
+  return result.results;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/config/constants.ts.html b/coverage/lcov-report/src/config/constants.ts.html new file mode 100644 index 0000000..a32186a --- /dev/null +++ b/coverage/lcov-report/src/config/constants.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for src/config/constants.ts + + + + + + + + + +
+
+

All files / src/config constants.ts

+
+ +
+ 100% + Statements + 5/5 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 5/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +143x +  +  +  +  +  +  +  +  +3x +3x +3x +3x + 
export const ALLOWED_ORIGINS = new Set([
+  "https://2dtiler.com",
+  "https://app.2dtiler.com",
+  "http://localhost:4321",
+  "http://127.0.0.1:4321",
+  "http://localhost:8787",
+  "http://127.0.0.1:8787",
+]);
+ 
+export const INTERNAL_API_KEY_HEADER = "X-Internal-Api-Key";
+export const LOSPEC_PALETTE_LIST_URL = "https://lospec.com/palette-list/load";
+export const LOSPEC_CDN_BASE_URL = "https://cdn.lospec.com/";
+export const LOSPEC_PALETTES_PAGE_SIZE = 100;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/config/index.html b/coverage/lcov-report/src/config/index.html new file mode 100644 index 0000000..27292e6 --- /dev/null +++ b/coverage/lcov-report/src/config/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for src/config + + + + + + + + + +
+
+

All files src/config

+
+ +
+ 100% + Statements + 5/5 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 5/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
constants.ts +
+
100%5/5100%0/0100%0/0100%5/5
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/index.html b/coverage/lcov-report/src/index.html new file mode 100644 index 0000000..6bd22b6 --- /dev/null +++ b/coverage/lcov-report/src/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for src + + + + + + + + + +
+
+

All files src

+
+ +
+ 100% + Statements + 6/6 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 6/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
app.ts +
+
100%4/4100%0/0100%0/0100%4/4
index.ts +
+
100%2/2100%0/0100%1/1100%2/2
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/index.ts.html b/coverage/lcov-report/src/index.ts.html new file mode 100644 index 0000000..048c3a3 --- /dev/null +++ b/coverage/lcov-report/src/index.ts.html @@ -0,0 +1,133 @@ + + + + + + Code coverage report for src/index.ts + + + + + + + + + +
+
+

All files / src index.ts

+
+ +
+ 100% + Statements + 2/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 2/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  + 
import app from "./app";
+import { syncPalettes } from "./jobs/sync-palettes";
+import type { Env } from "./config/types";
+ 
+export default {
+  fetch: app.fetch,
+ 
+  async scheduled(
+    _event: ScheduledEvent,
+    env: Env,
+    ctx: ExecutionContext,
+  ): Promise<void> {
+    const signal = AbortSignal.timeout(15 * 60 * 1000);
+    ctx.waitUntil(syncPalettes(env, signal));
+  },
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/jobs/index.html b/coverage/lcov-report/src/jobs/index.html new file mode 100644 index 0000000..5ade71f --- /dev/null +++ b/coverage/lcov-report/src/jobs/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for src/jobs + + + + + + + + + +
+
+

All files src/jobs

+
+ +
+ 100% + Statements + 27/27 +
+ + +
+ 100% + Branches + 6/6 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 27/27 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
sync-palettes.ts +
+
100%27/27100%6/6100%3/3100%27/27
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/src/jobs/sync-palettes.ts.html b/coverage/lcov-report/src/jobs/sync-palettes.ts.html new file mode 100644 index 0000000..3e232be --- /dev/null +++ b/coverage/lcov-report/src/jobs/sync-palettes.ts.html @@ -0,0 +1,271 @@ + + + + + + Code coverage report for src/jobs/sync-palettes.ts + + + + + + + + + +
+
+

All files / src/jobs sync-palettes.ts

+
+ +
+ 100% + Statements + 27/27 +
+ + +
+ 100% + Branches + 6/6 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 27/27 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63  +  +  +  +  +  +  +  +  +  +  +4x +4x +4x +  +4x +5x +5x +3x +1x +1x +  +  +2x +  +2x +  +2x +2x +  +2x +  +2x +2x +  +2x +  +  +  +2x +1x +  +  +1x +  +  +1x +  +2x +1x +1x +  +  +1x +1x +  +  +  +3x +  +  +  + 
import type { Env } from "../config/types";
+import { fetchLospecPalettePage } from "../app/services/lospec-api";
+import {
+  getExistingPaletteIds,
+  insertPalettes,
+} from "../app/services/lospec-palettes-repository";
+ 
+export async function syncPalettes(
+  env: Env,
+  signal: AbortSignal,
+): Promise<void> {
+  let page = 0;
+  let pagesProcessed = 0;
+  let totalInserted = 0;
+ 
+  while (!signal.aborted) {
+    try {
+      const palettes = await fetchLospecPalettePage(page, signal);
+      if (palettes.length === 0) {
+        console.log(`Page ${page}: no palettes returned; stopping pagination`);
+        break;
+      }
+ 
+      const existingIds = await getExistingPaletteIds(
+        env.DB,
+        palettes.map((palette) => palette._id),
+      );
+      const unseenPalettes = palettes.filter(
+        (palette) => !existingIds.has(palette._id),
+      );
+      const inserted = await insertPalettes(env.DB, unseenPalettes);
+ 
+      pagesProcessed++;
+      totalInserted += inserted;
+ 
+      console.log(
+        `Page ${page}: fetched ${palettes.length}, inserted ${inserted}, existing ${existingIds.size}`,
+      );
+ 
+      if (existingIds.size > 0) {
+        console.log(
+          `Page ${page}: encountered existing palette IDs; stopping pagination`,
+        );
+        break;
+      }
+ 
+      page++;
+    } catch (error) {
+      if (signal.aborted) {
+        console.warn("Palette sync timed out; stopping early");
+        break;
+      }
+ 
+      console.error(`Page ${page}: failed to sync palettes`, error);
+      return;
+    }
+  }
+ 
+  console.log(
+    `Palette sync complete: inserted ${totalInserted} palettes across ${pagesProcessed} page(s)`,
+  );
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..8b60543 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,385 @@ +TN: +SF:src/app.ts +FNF:0 +FNH:0 +DA:8,2 +DA:10,2 +DA:12,2 +DA:13,2 +LF:4 +LH:4 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/index.ts +FN:8,(anonymous_0) +FNF:1 +FNH:1 +FNDA:1,(anonymous_0) +DA:13,1 +DA:14,1 +LF:2 +LH:2 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/app/controllers/health.ts +FN:5,getHealth +FNF:1 +FNH:1 +FNDA:3,getHealth +DA:6,3 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/app/controllers/lospec-palettes.ts +FN:11,parsePage +FN:23,normalizeQueryValue +FN:28,parseListLospecPalettesOptions +FN:43,getLospecPalettes +FNF:4 +FNH:4 +FNDA:3,parsePage +FNDA:4,normalizeQueryValue +FNDA:3,parseListLospecPalettesOptions +FNDA:3,getLospecPalettes +DA:12,3 +DA:13,1 +DA:16,2 +DA:17,1 +DA:20,1 +DA:24,4 +DA:25,4 +DA:31,3 +DA:32,3 +DA:33,1 +DA:36,2 +DA:46,3 +DA:47,3 +DA:48,1 +DA:56,2 +DA:57,2 +DA:58,2 +DA:59,1 +DA:62,1 +DA:63,1 +LF:20 +LH:20 +BRDA:12,0,0,1 +BRDA:12,0,1,2 +BRDA:16,1,0,1 +BRDA:16,1,1,1 +BRDA:25,2,0,2 +BRDA:25,2,1,2 +BRDA:32,3,0,1 +BRDA:32,3,1,2 +BRDA:47,4,0,1 +BRDA:47,4,1,2 +BRDA:56,5,0,2 +BRDA:56,5,1,2 +BRDA:58,6,0,1 +BRDA:58,6,1,1 +BRF:14 +BRH:14 +end_of_record +TN: +SF:src/app/middleware/cors.ts +FN:9,applyCorsHeaders +FN:16,(anonymous_1) +FNF:2 +FNH:2 +FNDA:2,applyCorsHeaders +FNDA:8,(anonymous_1) +DA:10,2 +DA:11,2 +DA:12,2 +DA:13,2 +DA:16,2 +DA:20,8 +DA:21,8 +DA:23,8 +DA:24,1 +DA:25,1 +DA:28,7 +DA:29,4 +DA:30,4 +DA:33,3 +DA:34,1 +DA:37,2 +DA:38,1 +DA:39,1 +DA:40,1 +DA:43,1 +DA:44,1 +LF:21 +LH:21 +BRDA:23,0,0,1 +BRDA:23,0,1,7 +BRDA:23,1,0,8 +BRDA:23,1,1,8 +BRDA:28,2,0,4 +BRDA:28,2,1,3 +BRDA:33,3,0,1 +BRDA:33,3,1,2 +BRDA:37,4,0,1 +BRDA:37,4,1,1 +BRF:10 +BRH:10 +end_of_record +TN: +SF:src/app/models/lospec-palette.ts +FN:50,isRecord +FN:54,isLospecPaletteApiItem +FN:60,asNullableString +FN:64,normalizePaletteUser +FN:76,normalizePaletteExampleImage +FN:80,normalizePaletteExamples +FN:87,(anonymous_6) +FN:106,serializeJsonField +FN:114,deserializeJsonField +FN:126,mapPaletteToRow +FN:142,mapRowToResponse +FNF:11 +FNH:11 +FNDA:11,isRecord +FNDA:4,isLospecPaletteApiItem +FNDA:14,asNullableString +FNDA:3,normalizePaletteUser +FNDA:3,normalizePaletteExampleImage +FNDA:3,normalizePaletteExamples +FNDA:4,(anonymous_6) +FNDA:3,serializeJsonField +FNDA:8,deserializeJsonField +FNDA:1,mapPaletteToRow +FNDA:2,mapRowToResponse +DA:51,11 +DA:57,4 +DA:61,14 +DA:65,3 +DA:66,0 +DA:69,3 +DA:70,0 +DA:73,3 +DA:77,3 +DA:83,3 +DA:84,0 +DA:87,3 +DA:88,4 +DA:89,0 +DA:92,4 +DA:93,4 +DA:94,1 +DA:97,3 +DA:107,3 +DA:108,0 +DA:111,3 +DA:115,8 +DA:116,0 +DA:119,8 +DA:120,8 +DA:122,1 +DA:129,1 +DA:143,2 +DA:144,2 +DA:146,2 +LF:30 +LH:24 +BRDA:51,0,0,11 +BRDA:51,0,1,11 +BRDA:57,1,0,4 +BRDA:57,1,1,4 +BRDA:61,2,0,13 +BRDA:61,2,1,1 +BRDA:65,3,0,0 +BRDA:65,3,1,3 +BRDA:69,4,0,0 +BRDA:69,4,1,3 +BRDA:83,5,0,0 +BRDA:83,5,1,3 +BRDA:88,6,0,0 +BRDA:88,6,1,4 +BRDA:93,7,0,1 +BRDA:93,7,1,3 +BRDA:107,8,0,0 +BRDA:107,8,1,3 +BRDA:107,9,0,3 +BRDA:107,9,1,3 +BRDA:115,10,0,0 +BRDA:115,10,1,8 +BRF:22 +BRH:16 +end_of_record +TN: +SF:src/app/routes/health.ts +FN:6,registerHealthRoutes +FNF:1 +FNH:1 +FNDA:2,registerHealthRoutes +DA:7,2 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/app/routes/lospec-palettes.ts +FN:6,registerLospecPaletteRoutes +FNF:1 +FNH:1 +FNDA:2,registerLospecPaletteRoutes +DA:7,2 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/app/services/lospec-api.ts +FN:7,fetchLospecPalettePage +FNF:1 +FNH:1 +FNDA:3,fetchLospecPalettePage +DA:11,3 +DA:12,3 +DA:13,3 +DA:14,3 +DA:15,3 +DA:17,3 +DA:18,3 +DA:19,1 +DA:22,2 +DA:23,2 +DA:24,1 +DA:27,1 +LF:12 +LH:12 +BRDA:18,0,0,1 +BRDA:18,0,1,2 +BRDA:23,1,0,1 +BRDA:23,1,1,1 +BRF:4 +BRH:4 +end_of_record +TN: +SF:src/app/services/lospec-palettes-repository.ts +FN:9,getExistingPaletteIds +FN:17,(anonymous_1) +FN:23,(anonymous_2) +FN:26,insertPalettes +FN:34,(anonymous_4) +FN:68,listPalettes +FNF:6 +FNH:6 +FNDA:2,getExistingPaletteIds +FNDA:2,(anonymous_1) +FNDA:2,(anonymous_2) +FNDA:2,insertPalettes +FNDA:1,(anonymous_4) +FNDA:1,listPalettes +DA:13,2 +DA:14,1 +DA:17,2 +DA:18,1 +DA:21,1 +DA:23,2 +DA:30,2 +DA:31,1 +DA:34,1 +DA:35,1 +DA:37,1 +DA:64,1 +DA:65,1 +DA:72,1 +DA:73,1 +DA:75,1 +DA:76,1 +DA:77,1 +DA:80,1 +DA:81,1 +DA:86,1 +DA:90,1 +DA:91,1 +DA:93,1 +DA:113,1 +LF:25 +LH:25 +BRDA:13,0,0,1 +BRDA:13,0,1,1 +BRDA:30,1,0,1 +BRDA:30,1,1,1 +BRDA:75,2,0,1 +BRDA:75,2,1,0 +BRDA:80,3,0,1 +BRDA:80,3,1,0 +BRDA:90,4,0,1 +BRDA:90,4,1,0 +BRF:10 +BRH:7 +end_of_record +TN: +SF:src/config/constants.ts +FNF:0 +FNH:0 +DA:1,3 +DA:10,3 +DA:11,3 +DA:12,3 +DA:13,3 +LF:5 +LH:5 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/jobs/sync-palettes.ts +FN:8,syncPalettes +FN:26,(anonymous_1) +FN:28,(anonymous_2) +FNF:3 +FNH:3 +FNDA:4,syncPalettes +FNDA:2,(anonymous_1) +FNDA:2,(anonymous_2) +DA:12,4 +DA:13,4 +DA:14,4 +DA:16,4 +DA:17,5 +DA:18,5 +DA:19,3 +DA:20,1 +DA:21,1 +DA:24,2 +DA:26,2 +DA:28,2 +DA:29,2 +DA:31,2 +DA:33,2 +DA:34,2 +DA:36,2 +DA:40,2 +DA:41,1 +DA:44,1 +DA:47,1 +DA:49,2 +DA:50,1 +DA:51,1 +DA:54,1 +DA:55,1 +DA:59,3 +LF:27 +LH:27 +BRDA:19,0,0,1 +BRDA:19,0,1,2 +BRDA:40,1,0,1 +BRDA:40,1,1,1 +BRDA:49,2,0,1 +BRDA:49,2,1,1 +BRF:6 +BRH:6 +end_of_record diff --git a/package.json b/package.json index 191d6b9..00a54d5 100644 --- a/package.json +++ b/package.json @@ -6,17 +6,21 @@ "dev": "wrangler dev", "deploy": "wrangler deploy", "lint": "eslint src", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest run --coverage" }, "dependencies": { "hono": "^4.12.12" }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.14.3", "@cloudflare/workers-types": "^4.20260410.1", "@eslint/js": "^10.0.1", + "@vitest/coverage-istanbul": "^4.1.4", "eslint": "^10.2.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.1", + "vitest": "^4.1.4", "wrangler": "^4.81.1" } } \ No newline at end of file diff --git a/tests/app.test.ts b/tests/app.test.ts new file mode 100644 index 0000000..eb29321 --- /dev/null +++ b/tests/app.test.ts @@ -0,0 +1,196 @@ +import { describe, expect, it } from "vitest"; + +import app from "../src/app"; +import type { LospecPaletteRow } from "../src/app/models/lospec-palette"; +import { LOSPEC_PALETTES_PAGE_SIZE } from "../src/config/constants"; +import { createTestEnv } from "./helpers/env"; + +function createListPalettesDb(rows: LospecPaletteRow[]) { + let sql = ""; + let bindings: Array = []; + + const db = { + prepare(statement: string) { + sql = statement; + + return { + bind(...values: Array) { + bindings = values; + + return { + all: async () => ({ results: rows }), + }; + }, + }; + }, + } as unknown as D1Database; + + return { + db, + getSql: () => sql, + getBindings: () => bindings, + }; +} + +describe("app", () => { + it("returns an empty JSON body for GET /", async () => { + const response = await app.request("/", {}, createTestEnv()); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({}); + }); + + it("applies CORS headers for allowed origins", async () => { + const response = await app.request( + "/", + { + headers: new Headers({ Origin: "http://localhost:4321" }), + }, + createTestEnv(), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("Access-Control-Allow-Origin")).toBe( + "http://localhost:4321", + ); + expect(response.headers.get("Access-Control-Allow-Methods")).toBe( + "GET, OPTIONS", + ); + expect(response.headers.get("Access-Control-Allow-Headers")).toBe( + "Content-Type", + ); + expect(response.headers.get("Vary")).toBe("Origin"); + }); + + it("handles OPTIONS preflight requests for allowed origins", async () => { + const response = await app.request( + "/", + { + method: "OPTIONS", + headers: new Headers({ Origin: "http://localhost:4321" }), + }, + createTestEnv(), + ); + + expect(response.status).toBe(204); + expect(response.headers.get("Access-Control-Allow-Origin")).toBe( + "http://localhost:4321", + ); + expect(response.headers.get("Access-Control-Allow-Methods")).toBe( + "GET, OPTIONS", + ); + expect(response.headers.get("Access-Control-Allow-Headers")).toBe( + "Content-Type", + ); + expect(await response.text()).toBe(""); + }); + + it("rejects disallowed origins", async () => { + const response = await app.request( + "/", + { + headers: new Headers({ Origin: "https://evil.example" }), + }, + createTestEnv(), + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ error: "Forbidden origin" }); + }); + + it("allows internal requests to bypass origin checks", async () => { + const response = await app.request( + "/", + { + headers: new Headers({ + Origin: "https://evil.example", + "X-Internal-Api-Key": "test-internal-api-key", + }), + }, + createTestEnv(), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("Access-Control-Allow-Origin")).toBeNull(); + }); + + it("returns 400 for an invalid lospec page query", async () => { + const response = await app.request( + "/lospec_palettes?page=abc", + {}, + createTestEnv(), + ); + + expect(response.status).toBe(400); + expect(await response.json()).toEqual({ + error: `Invalid page parameter. Expected a non-negative integer with ${LOSPEC_PALETTES_PAGE_SIZE} results per page.`, + }); + }); + + it("returns 429 when the rate limiter rejects the request", async () => { + const response = await app.request( + "/lospec_palettes", + {}, + createTestEnv({ + RATE_LIMITER: { + limit: async () => ({ success: false }), + }, + }), + ); + + expect(response.status).toBe(429); + expect(await response.json()).toEqual({ + error: "Rate limit exceeded. Try again in an hour.", + }); + }); + + it("lists palettes with normalized response data and query filters", async () => { + const database = createListPalettesDb([ + { + id: "sunset-1", + title: "Sunset", + slug: "sunset", + description: "Warm palette", + tags: JSON.stringify(["warm", "sky"]), + user: JSON.stringify({ name: "alice" }), + colors: JSON.stringify(["#ff6600", "#220044"]), + examples: JSON.stringify([ + { + image: "/pixel-art/sunset.png", + description: "Preview", + }, + ]), + published_at: "2026-04-01T00:00:00.000Z", + }, + ]); + + const response = await app.request( + "/lospec_palettes?page=1&search=%20Sunset%20&tags=%20Warm%20", + {}, + createTestEnv({ DB: database.db }), + ); + + expect(response.status).toBe(200); + expect(database.getSql()).toContain("LOWER(COALESCE(title, '')) LIKE ?"); + expect(database.getSql()).toContain("FROM json_each(lospec_palettes.tags)"); + expect(database.getBindings()).toEqual(["%sunset%", "warm", 100, 100]); + expect(await response.json()).toEqual([ + { + id: "sunset-1", + title: "Sunset", + slug: "sunset", + description: "Warm palette", + tags: ["warm", "sky"], + user: "alice", + colors: ["#ff6600", "#220044"], + examples: [ + { + image: "https://cdn.lospec.com/pixel-art/sunset.png", + description: "Preview", + }, + ], + published_at: "2026-04-01T00:00:00.000Z", + }, + ]); + }); +}); diff --git a/tests/helpers/env.ts b/tests/helpers/env.ts new file mode 100644 index 0000000..167612b --- /dev/null +++ b/tests/helpers/env.ts @@ -0,0 +1,31 @@ +import type { Env, RateLimiter } from "../../src/config/types"; + +function createDbStub(): D1Database { + return { + prepare() { + throw new Error("DB stub should not be used in this test."); + }, + batch() { + throw new Error("DB stub should not be used in this test."); + }, + dump() { + throw new Error("DB stub should not be used in this test."); + }, + exec() { + throw new Error("DB stub should not be used in this test."); + }, + } as unknown as D1Database; +} + +export function createTestEnv(overrides: Partial = {}): Env { + const rateLimiter: RateLimiter = { + limit: async () => ({ success: true }), + }; + + return { + DB: createDbStub(), + RATE_LIMITER: rateLimiter, + INTERNAL_API_KEY: "test-internal-api-key", + ...overrides, + }; +} diff --git a/tests/models-and-services.test.ts b/tests/models-and-services.test.ts new file mode 100644 index 0000000..f046f6c --- /dev/null +++ b/tests/models-and-services.test.ts @@ -0,0 +1,212 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { + isLospecPaletteApiItem, + mapRowToResponse, +} from "../src/app/models/lospec-palette"; +import { fetchLospecPalettePage } from "../src/app/services/lospec-api"; +import { + getExistingPaletteIds, + insertPalettes, +} from "../src/app/services/lospec-palettes-repository"; + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe("lospec palette models", () => { + it("accepts API items with an id", () => { + expect(isLospecPaletteApiItem({ _id: "palette-1" })).toBe(true); + expect(isLospecPaletteApiItem({ title: "missing id" })).toBe(false); + }); + + it("maps rows back into API responses", () => { + const response = mapRowToResponse({ + id: "palette-1", + title: "Palette", + slug: "palette", + description: "Example", + tags: "not-json", + user: JSON.stringify({ name: "alice" }), + colors: JSON.stringify(["#ffffff"]), + examples: JSON.stringify([ + { + image: "/sprites/example.png", + description: "Preview", + }, + ]), + published_at: "2026-04-02T00:00:00.000Z", + }); + + expect(response).toEqual({ + id: "palette-1", + title: "Palette", + slug: "palette", + description: "Example", + tags: "not-json", + user: "alice", + colors: ["#ffffff"], + examples: [ + { + image: "https://cdn.lospec.com/sprites/example.png", + description: "Preview", + }, + ], + published_at: "2026-04-02T00:00:00.000Z", + }); + }); +}); + +describe("lospec API service", () => { + it("fetches and filters a page of Lospec palettes", async () => { + const fetchMock = vi.fn(async (url: URL, init?: RequestInit) => { + expect(url.searchParams.get("colorNumberFilterType")).toBe("any"); + expect(url.searchParams.get("page")).toBe("3"); + expect(url.searchParams.get("tag")).toBe(""); + expect(url.searchParams.get("sortingType")).toBe("newest"); + expect(init?.signal).toBeDefined(); + + return new Response( + JSON.stringify({ + palettes: [{ _id: "palette-1" }, { title: "missing id" }], + }), + { status: 200 }, + ); + }); + + vi.stubGlobal("fetch", fetchMock); + + const result = await fetchLospecPalettePage( + 3, + new AbortController().signal, + ); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(result).toEqual([{ _id: "palette-1" }]); + }); + + it("returns an empty array when the payload has no palettes list", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(JSON.stringify({}), { status: 200 })), + ); + + const result = await fetchLospecPalettePage( + 0, + new AbortController().signal, + ); + + expect(result).toEqual([]); + }); + + it("throws when Lospec returns a non-success response", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response("upstream error", { status: 503 })), + ); + + await expect( + fetchLospecPalettePage(4, new AbortController().signal), + ).rejects.toThrow("Failed to fetch page 4: HTTP 503"); + }); +}); + +describe("lospec palette repository", () => { + it("skips querying when there are no ids", async () => { + const prepare = vi.fn(); + const database = { prepare } as unknown as D1Database; + + const result = await getExistingPaletteIds(database, []); + + expect(prepare).not.toHaveBeenCalled(); + expect(result).toEqual(new Set()); + }); + + it("loads existing palette ids from D1", async () => { + const all = vi.fn(async () => ({ + results: [{ id: "palette-1" }, { id: "palette-2" }], + })); + const bind = vi.fn(() => ({ all })); + const prepare = vi.fn(() => ({ bind })); + const database = { prepare } as unknown as D1Database; + + const result = await getExistingPaletteIds(database, [ + "palette-1", + "palette-2", + ]); + + expect(prepare).toHaveBeenCalledWith( + "SELECT id FROM lospec_palettes WHERE id IN (?, ?)", + ); + expect(bind).toHaveBeenCalledWith("palette-1", "palette-2"); + expect(result).toEqual(new Set(["palette-1", "palette-2"])); + }); + + it("inserts normalized palette rows in a batch", async () => { + const preparedStatements: Array<{ sql: string; bindings: unknown[] }> = []; + const batch = vi.fn(async (statements: unknown[]) => statements); + const database = { + prepare(sql: string) { + return { + bind(...bindings: unknown[]) { + const statement = { sql, bindings }; + preparedStatements.push(statement); + return statement; + }, + }; + }, + batch, + } as unknown as D1Database; + + const inserted = await insertPalettes(database, [ + { + _id: "palette-1", + title: "Palette", + slug: "palette", + description: "Example", + tags: ["warm"], + user: { name: "alice" }, + colors: ["#ffffff"], + examples: [ + { image: "/sprites/example.png", description: "Preview" }, + { description: "Ignored because it has no image" }, + ], + publishedAt: "2026-04-02T00:00:00.000Z", + }, + ]); + + expect(inserted).toBe(1); + expect(batch).toHaveBeenCalledWith(preparedStatements); + expect(preparedStatements).toHaveLength(1); + expect(preparedStatements[0].sql).toContain( + "INSERT OR IGNORE INTO lospec_palettes", + ); + expect(preparedStatements[0].bindings).toEqual([ + "palette-1", + "Palette", + "palette", + "Example", + JSON.stringify(["warm"]), + "alice", + JSON.stringify(["#ffffff"]), + JSON.stringify([ + { + image: "https://cdn.lospec.com/sprites/example.png", + description: "Preview", + }, + ]), + "2026-04-02T00:00:00.000Z", + ]); + }); + + it("returns zero inserts without calling batch for an empty palette list", async () => { + const batch = vi.fn(); + const database = { batch } as unknown as D1Database; + + const inserted = await insertPalettes(database, []); + + expect(inserted).toBe(0); + expect(batch).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/sync-palettes.test.ts b/tests/sync-palettes.test.ts new file mode 100644 index 0000000..af5c2b0 --- /dev/null +++ b/tests/sync-palettes.test.ts @@ -0,0 +1,110 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { Env } from "../src/config/types"; +import { fetchLospecPalettePage } from "../src/app/services/lospec-api"; +import { + getExistingPaletteIds, + insertPalettes, +} from "../src/app/services/lospec-palettes-repository"; +import { syncPalettes } from "../src/jobs/sync-palettes"; + +vi.mock("../src/app/services/lospec-api", () => ({ + fetchLospecPalettePage: vi.fn(), +})); + +vi.mock("../src/app/services/lospec-palettes-repository", () => ({ + getExistingPaletteIds: vi.fn(), + insertPalettes: vi.fn(), +})); + +function createEnv(): Env { + return { + DB: {} as D1Database, + RATE_LIMITER: { + limit: async () => ({ success: true }), + }, + INTERNAL_API_KEY: "test-key", + }; +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.clearAllMocks(); +}); + +describe("syncPalettes", () => { + it("syncs pages until Lospec returns an empty page", async () => { + const env = createEnv(); + const fetchMock = vi.mocked(fetchLospecPalettePage); + const existingIdsMock = vi.mocked(getExistingPaletteIds); + const insertMock = vi.mocked(insertPalettes); + + fetchMock + .mockResolvedValueOnce([{ _id: "palette-1" }]) + .mockResolvedValueOnce([]); + existingIdsMock.mockResolvedValue(new Set()); + insertMock.mockResolvedValue(1); + vi.spyOn(console, "log").mockImplementation(() => {}); + + await syncPalettes(env, new AbortController().signal); + + expect(fetchMock).toHaveBeenNthCalledWith(1, 0, expect.any(AbortSignal)); + expect(fetchMock).toHaveBeenNthCalledWith(2, 1, expect.any(AbortSignal)); + expect(existingIdsMock).toHaveBeenCalledWith(env.DB, ["palette-1"]); + expect(insertMock).toHaveBeenCalledWith(env.DB, [{ _id: "palette-1" }]); + }); + + it("stops when it encounters existing palette ids", async () => { + const env = createEnv(); + const fetchMock = vi.mocked(fetchLospecPalettePage); + const existingIdsMock = vi.mocked(getExistingPaletteIds); + const insertMock = vi.mocked(insertPalettes); + + fetchMock.mockResolvedValueOnce([{ _id: "palette-1" }]); + existingIdsMock.mockResolvedValueOnce(new Set(["palette-1"])); + insertMock.mockResolvedValue(0); + vi.spyOn(console, "log").mockImplementation(() => {}); + + await syncPalettes(env, new AbortController().signal); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(insertMock).toHaveBeenCalledWith(env.DB, []); + }); + + it("warns instead of erroring when the sync times out", async () => { + const env = createEnv(); + const controller = new AbortController(); + const fetchMock = vi.mocked(fetchLospecPalettePage); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(console, "log").mockImplementation(() => {}); + + fetchMock.mockImplementationOnce(async () => { + controller.abort(); + throw new Error("timed out"); + }); + + await syncPalettes(env, controller.signal); + + expect(warnSpy).toHaveBeenCalledWith( + "Palette sync timed out; stopping early", + ); + expect(errorSpy).not.toHaveBeenCalled(); + }); + + it("logs and returns when a page fails to sync", async () => { + const env = createEnv(); + const fetchMock = vi.mocked(fetchLospecPalettePage); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(console, "log").mockImplementation(() => {}); + + fetchMock.mockRejectedValueOnce(new Error("boom")); + + await syncPalettes(env, new AbortController().signal); + + expect(errorSpy).toHaveBeenCalledWith( + "Page 0: failed to sync palettes", + expect.any(Error), + ); + }); +}); diff --git a/tests/worker.test.ts b/tests/worker.test.ts new file mode 100644 index 0000000..4730617 --- /dev/null +++ b/tests/worker.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { Env } from "../src/config/types"; +import worker from "../src/index"; +import { syncPalettes } from "../src/jobs/sync-palettes"; + +vi.mock("../src/jobs/sync-palettes", () => ({ + syncPalettes: vi.fn(async () => {}), +})); + +function createEnv(): Env { + return { + DB: {} as D1Database, + RATE_LIMITER: { + limit: async () => ({ success: true }), + }, + INTERNAL_API_KEY: "test-key", + }; +} + +describe("worker entrypoint", () => { + it("schedules palette sync with a timeout signal", async () => { + const env = createEnv(); + const waitUntil = vi.fn(); + + await worker.scheduled({} as ScheduledEvent, env, { + waitUntil, + } as ExecutionContext); + + expect(vi.mocked(syncPalettes)).toHaveBeenCalledWith( + env, + expect.any(AbortSignal), + ); + expect(waitUntil).toHaveBeenCalledTimes(1); + await expect(waitUntil.mock.calls[0][0]).resolves.toBeUndefined(); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..0193e87 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,28 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.toml", + }, + }), + ], + test: { + include: ["tests/**/*.test.ts"], + coverage: { + provider: "istanbul", + reporter: ["text", "lcov"], + include: ["src/**/*.ts"], + exclude: ["src/config/types.ts"], + reportOnFailure: true, + thresholds: { + lines: 80.01, + functions: 80.01, + branches: 80.01, + statements: 80.01, + }, + }, + }, +}); From 034b974a96fcc6985a469ae25531661454294537 Mon Sep 17 00:00:00 2001 From: Werner Bihl Date: Fri, 10 Apr 2026 15:07:16 +0200 Subject: [PATCH 5/7] Remove generated code coverage reports and related files --- .gitignore | 3 +- coverage/lcov-report/base.css | 224 ------- coverage/lcov-report/block-navigation.js | 87 --- coverage/lcov-report/favicon.png | Bin 445 -> 0 bytes coverage/lcov-report/index.html | 221 ------- coverage/lcov-report/prettify.css | 1 - coverage/lcov-report/prettify.js | 2 - coverage/lcov-report/sort-arrow-sprite.png | Bin 138 -> 0 bytes coverage/lcov-report/sorter.js | 210 ------- coverage/lcov-report/src/app.ts.html | 130 ---- .../src/app/controllers/health.ts.html | 106 ---- .../src/app/controllers/index.html | 131 ----- .../app/controllers/lospec-palettes.ts.html | 277 --------- .../src/app/middleware/cors.ts.html | 220 ------- .../lcov-report/src/app/middleware/index.html | 116 ---- .../lcov-report/src/app/models/index.html | 116 ---- .../src/app/models/lospec-palette.ts.html | 556 ------------------ .../lcov-report/src/app/routes/health.ts.html | 109 ---- .../lcov-report/src/app/routes/index.html | 131 ----- .../src/app/routes/lospec-palettes.ts.html | 109 ---- .../lcov-report/src/app/services/index.html | 131 ----- .../src/app/services/lospec-api.ts.html | 169 ------ .../lospec-palettes-repository.ts.html | 427 -------------- .../lcov-report/src/config/constants.ts.html | 124 ---- coverage/lcov-report/src/config/index.html | 116 ---- coverage/lcov-report/src/index.html | 131 ----- coverage/lcov-report/src/index.ts.html | 133 ----- coverage/lcov-report/src/jobs/index.html | 116 ---- .../src/jobs/sync-palettes.ts.html | 271 --------- coverage/lcov.info | 385 ------------ 30 files changed, 2 insertions(+), 4750 deletions(-) delete mode 100644 coverage/lcov-report/base.css delete mode 100644 coverage/lcov-report/block-navigation.js delete mode 100644 coverage/lcov-report/favicon.png delete mode 100644 coverage/lcov-report/index.html delete mode 100644 coverage/lcov-report/prettify.css delete mode 100644 coverage/lcov-report/prettify.js delete mode 100644 coverage/lcov-report/sort-arrow-sprite.png delete mode 100644 coverage/lcov-report/sorter.js delete mode 100644 coverage/lcov-report/src/app.ts.html delete mode 100644 coverage/lcov-report/src/app/controllers/health.ts.html delete mode 100644 coverage/lcov-report/src/app/controllers/index.html delete mode 100644 coverage/lcov-report/src/app/controllers/lospec-palettes.ts.html delete mode 100644 coverage/lcov-report/src/app/middleware/cors.ts.html delete mode 100644 coverage/lcov-report/src/app/middleware/index.html delete mode 100644 coverage/lcov-report/src/app/models/index.html delete mode 100644 coverage/lcov-report/src/app/models/lospec-palette.ts.html delete mode 100644 coverage/lcov-report/src/app/routes/health.ts.html delete mode 100644 coverage/lcov-report/src/app/routes/index.html delete mode 100644 coverage/lcov-report/src/app/routes/lospec-palettes.ts.html delete mode 100644 coverage/lcov-report/src/app/services/index.html delete mode 100644 coverage/lcov-report/src/app/services/lospec-api.ts.html delete mode 100644 coverage/lcov-report/src/app/services/lospec-palettes-repository.ts.html delete mode 100644 coverage/lcov-report/src/config/constants.ts.html delete mode 100644 coverage/lcov-report/src/config/index.html delete mode 100644 coverage/lcov-report/src/index.html delete mode 100644 coverage/lcov-report/src/index.ts.html delete mode 100644 coverage/lcov-report/src/jobs/index.html delete mode 100644 coverage/lcov-report/src/jobs/sync-palettes.ts.html delete mode 100644 coverage/lcov.info diff --git a/.gitignore b/.gitignore index 4e2b288..1692880 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ dist-ssr # Cloudflare/Wrangler .wrangler -.env \ No newline at end of file +.env +coverage diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css deleted file mode 100644 index f418035..0000000 --- a/coverage/lcov-report/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js deleted file mode 100644 index 530d1ed..0000000 --- a/coverage/lcov-report/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png deleted file mode 100644 index c1525b811a167671e9de1fa78aab9f5c0b61cef7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- 96.02% - Statements - 145/151 -
- - -
- 86.36% - Branches - 57/66 -
- - -
- 100% - Functions - 31/31 -
- - -
- 95.97% - Lines - 143/149 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
src -
-
100%6/6100%0/0100%1/1100%6/6
src/app/controllers -
-
100%21/21100%14/14100%5/5100%21/21
src/app/middleware -
-
100%21/21100%10/10100%2/2100%21/21
src/app/models -
-
80%24/3072.72%16/22100%11/1180%24/30
src/app/routes -
-
100%2/2100%0/0100%2/2100%2/2
src/app/services -
-
100%39/3978.57%11/14100%7/7100%37/37
src/config -
-
100%5/5100%0/0100%0/0100%5/5
src/jobs -
-
100%27/27100%6/6100%3/3100%27/27
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/coverage/lcov-report/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js deleted file mode 100644 index b322523..0000000 --- a/coverage/lcov-report/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png deleted file mode 100644 index 6ed68316eb3f65dec9063332d2f69bf3093bbfab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js deleted file mode 100644 index 4ed70ae..0000000 --- a/coverage/lcov-report/sorter.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? '' : 'none'; - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); diff --git a/coverage/lcov-report/src/app.ts.html b/coverage/lcov-report/src/app.ts.html deleted file mode 100644 index 9d623fd..0000000 --- a/coverage/lcov-report/src/app.ts.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - Code coverage report for src/app.ts - - - - - - - - - -
-
-

All files / src app.ts

-
- -
- 100% - Statements - 4/4 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 0/0 -
- - -
- 100% - Lines - 4/4 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16  -  -  -  -  -  -  -2x -  -2x -  -2x -2x -  -  - 
import { Hono } from "hono";
- 
-import { registerHealthRoutes } from "./app/routes/health";
-import { registerLospecPaletteRoutes } from "./app/routes/lospec-palettes";
-import { corsMiddleware } from "./app/middleware/cors";
-import type { AppBindings } from "./config/types";
- 
-const app = new Hono<AppBindings>();
- 
-app.use("*", corsMiddleware);
- 
-registerHealthRoutes(app);
-registerLospecPaletteRoutes(app);
- 
-export default app;
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/controllers/health.ts.html b/coverage/lcov-report/src/app/controllers/health.ts.html deleted file mode 100644 index 3c5418f..0000000 --- a/coverage/lcov-report/src/app/controllers/health.ts.html +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - Code coverage report for src/app/controllers/health.ts - - - - - - - - - -
-
-

All files / src/app/controllers health.ts

-
- -
- 100% - Statements - 1/1 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 1/1 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8  -  -  -  -  -3x -  - 
import type { Context } from "hono";
- 
-import type { AppBindings } from "../../config/types";
- 
-export function getHealth(c: Context<AppBindings>): Response {
-  return c.json({});
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/controllers/index.html b/coverage/lcov-report/src/app/controllers/index.html deleted file mode 100644 index 0632fde..0000000 --- a/coverage/lcov-report/src/app/controllers/index.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - Code coverage report for src/app/controllers - - - - - - - - - -
-
-

All files src/app/controllers

-
- -
- 100% - Statements - 21/21 -
- - -
- 100% - Branches - 14/14 -
- - -
- 100% - Functions - 5/5 -
- - -
- 100% - Lines - 21/21 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
health.ts -
-
100%1/1100%0/0100%1/1100%1/1
lospec-palettes.ts -
-
100%20/20100%14/14100%4/4100%20/20
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/controllers/lospec-palettes.ts.html b/coverage/lcov-report/src/app/controllers/lospec-palettes.ts.html deleted file mode 100644 index da8f6ed..0000000 --- a/coverage/lcov-report/src/app/controllers/lospec-palettes.ts.html +++ /dev/null @@ -1,277 +0,0 @@ - - - - - - Code coverage report for src/app/controllers/lospec-palettes.ts - - - - - - - - - -
-
-

All files / src/app/controllers lospec-palettes.ts

-
- -
- 100% - Statements - 20/20 -
- - -
- 100% - Branches - 14/14 -
- - -
- 100% - Functions - 4/4 -
- - -
- 100% - Lines - 20/20 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65  -  -  -  -  -  -  -  -  -  -  -3x -1x -  -  -2x -1x -  -  -1x -  -  -  -4x -4x -  -  -  -  -  -3x -3x -1x -  -  -2x -  -  -  -  -  -  -  -  -  -3x -3x -1x -  -  -  -  -  -  -  -2x -2x -2x -1x -  -  -1x -1x -  - 
import type { Context } from "hono";
- 
-import { LOSPEC_PALETTES_PAGE_SIZE } from "../../config/constants";
-import type { AppBindings } from "../../config/types";
-import {
-  mapRowToResponse,
-  type ListLospecPalettesOptions,
-} from "../models/lospec-palette";
-import { listPalettes } from "../services/lospec-palettes-repository";
- 
-function parsePage(value: string | undefined): number | null {
-  if (value === undefined) {
-    return 0;
-  }
- 
-  if (!/^\d+$/.test(value)) {
-    return null;
-  }
- 
-  return Number.parseInt(value, 10);
-}
- 
-function normalizeQueryValue(value: string | undefined): string | undefined {
-  const trimmed = value?.trim();
-  return trimmed ? trimmed : undefined;
-}
- 
-function parseListLospecPalettesOptions(
-  query: Record<string, string | undefined>,
-): ListLospecPalettesOptions | null {
-  const page = parsePage(query.page);
-  if (page === null) {
-    return null;
-  }
- 
-  return {
-    page,
-    search: normalizeQueryValue(query.search),
-    tag: normalizeQueryValue(query.tags),
-  };
-}
- 
-export async function getLospecPalettes(
-  c: Context<AppBindings>,
-): Promise<Response> {
-  const options = parseListLospecPalettesOptions(c.req.query());
-  if (!options) {
-    return c.json(
-      {
-        error: `Invalid page parameter. Expected a non-negative integer with ${LOSPEC_PALETTES_PAGE_SIZE} results per page.`,
-      },
-      400,
-    );
-  }
- 
-  const ip = c.req.raw.headers.get("CF-Connecting-IP") ?? "unknown";
-  const { success } = await c.env.RATE_LIMITER.limit({ key: ip });
-  if (!success) {
-    return c.json({ error: "Rate limit exceeded. Try again in an hour." }, 429);
-  }
- 
-  const palettes = await listPalettes(c.env.DB, options);
-  return c.json(palettes.map(mapRowToResponse));
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/middleware/cors.ts.html b/coverage/lcov-report/src/app/middleware/cors.ts.html deleted file mode 100644 index 2ab29a5..0000000 --- a/coverage/lcov-report/src/app/middleware/cors.ts.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - Code coverage report for src/app/middleware/cors.ts - - - - - - - - - -
-
-

All files / src/app/middleware cors.ts

-
- -
- 100% - Statements - 21/21 -
- - -
- 100% - Branches - 10/10 -
- - -
- 100% - Functions - 2/2 -
- - -
- 100% - Lines - 21/21 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46  -  -  -  -  -  -  -  -  -2x -2x -2x -2x -  -  -2x -  -  -  -8x -8x -  -8x -1x -1x -  -  -7x -4x -4x -  -  -3x -1x -  -  -2x -1x -1x -1x -  -  -1x -1x -  - 
import type { MiddlewareHandler } from "hono";
- 
-import {
-  ALLOWED_ORIGINS,
-  INTERNAL_API_KEY_HEADER,
-} from "../../config/constants";
-import type { AppBindings } from "../../config/types";
- 
-function applyCorsHeaders(headers: Headers, origin: string): void {
-  headers.set("Access-Control-Allow-Origin", origin);
-  headers.set("Access-Control-Allow-Methods", "GET, OPTIONS");
-  headers.set("Access-Control-Allow-Headers", "Content-Type");
-  headers.set("Vary", "Origin");
-}
- 
-export const corsMiddleware: MiddlewareHandler<AppBindings> = async (
-  c,
-  next,
-) => {
-  const origin = c.req.header("Origin");
-  const internalApiKey = c.req.header(INTERNAL_API_KEY_HEADER);
- 
-  if (c.env.INTERNAL_API_KEY && internalApiKey === c.env.INTERNAL_API_KEY) {
-    await next();
-    return;
-  }
- 
-  if (!origin) {
-    await next();
-    return;
-  }
- 
-  if (!ALLOWED_ORIGINS.has(origin)) {
-    return c.json({ error: "Forbidden origin" }, 403);
-  }
- 
-  if (c.req.method === "OPTIONS") {
-    const headers = new Headers();
-    applyCorsHeaders(headers, origin);
-    return new Response(null, { status: 204, headers });
-  }
- 
-  await next();
-  applyCorsHeaders(c.res.headers, origin);
-};
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/middleware/index.html b/coverage/lcov-report/src/app/middleware/index.html deleted file mode 100644 index 4df88a3..0000000 --- a/coverage/lcov-report/src/app/middleware/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for src/app/middleware - - - - - - - - - -
-
-

All files src/app/middleware

-
- -
- 100% - Statements - 21/21 -
- - -
- 100% - Branches - 10/10 -
- - -
- 100% - Functions - 2/2 -
- - -
- 100% - Lines - 21/21 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
cors.ts -
-
100%21/21100%10/10100%2/2100%21/21
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/models/index.html b/coverage/lcov-report/src/app/models/index.html deleted file mode 100644 index ee6eeca..0000000 --- a/coverage/lcov-report/src/app/models/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for src/app/models - - - - - - - - - -
-
-

All files src/app/models

-
- -
- 80% - Statements - 24/30 -
- - -
- 72.72% - Branches - 16/22 -
- - -
- 100% - Functions - 11/11 -
- - -
- 80% - Lines - 24/30 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
lospec-palette.ts -
-
80%24/3072.72%16/22100%11/1180%24/30
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/models/lospec-palette.ts.html b/coverage/lcov-report/src/app/models/lospec-palette.ts.html deleted file mode 100644 index 0feadc2..0000000 --- a/coverage/lcov-report/src/app/models/lospec-palette.ts.html +++ /dev/null @@ -1,556 +0,0 @@ - - - - - - Code coverage report for src/app/models/lospec-palette.ts - - - - - - - - - -
-
-

All files / src/app/models lospec-palette.ts

-
- -
- 80% - Statements - 24/30 -
- - -
- 72.72% - Branches - 16/22 -
- - -
- 100% - Functions - 11/11 -
- - -
- 80% - Lines - 24/30 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -11x -  -  -  -  -  -4x -  -  -  -14x -  -  -  -3x -  -  -  -3x -  -  -  -3x -  -  -  -3x -  -  -  -  -  -3x -  -  -  -3x -4x -  -  -  -4x -4x -1x -  -  -3x -  -  -  -  -  -  -  -  -  -3x -  -  -  -3x -  -  -  -8x -  -  -  -8x -8x -  -1x -  -  -  -  -  -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -2x -2x -  -2x -  -  -  -  -  -  -  -  -  -  -  - 
import { LOSPEC_CDN_BASE_URL } from "../../config/constants";
- 
-export interface LospecPaletteApiItem {
-  _id: string;
-  title?: string;
-  slug?: string;
-  description?: string;
-  tags?: unknown;
-  user?: unknown;
-  colors?: unknown;
-  examples?: unknown;
-  publishedAt?: string;
-}
- 
-export interface LospecPaletteRow {
-  id: string;
-  title: string | null;
-  slug: string | null;
-  description: string | null;
-  tags: string | null;
-  user: string | null;
-  colors: string | null;
-  examples: string | null;
-  published_at: string | null;
-}
- 
-export interface LospecPaletteExample {
-  image: string;
-  description: string | null;
-}
- 
-export interface LospecPaletteResponse {
-  id: string;
-  title: string | null;
-  slug: string | null;
-  description: string | null;
-  tags: unknown | null;
-  user: string | null;
-  colors: unknown | null;
-  examples: LospecPaletteExample[] | null;
-  published_at: string | null;
-}
- 
-export interface ListLospecPalettesOptions {
-  page: number;
-  search?: string;
-  tag?: string;
-}
- 
-function isRecord(value: unknown): value is Record<string, unknown> {
-  return typeof value === "object" && value !== null;
-}
- 
-export function isLospecPaletteApiItem(
-  value: unknown,
-): value is LospecPaletteApiItem {
-  return isRecord(value) && typeof value._id === "string";
-}
- 
-function asNullableString(value: unknown): string | null {
-  return typeof value === "string" ? value : null;
-}
- 
-function normalizePaletteUser(value: unknown): string | null {
-  Iif (typeof value === "string") {
-    return value;
-  }
- 
-  Iif (!isRecord(value)) {
-    return null;
-  }
- 
-  return asNullableString(value.name);
-}
- 
-function normalizePaletteExampleImage(value: string): string {
-  return new URL(value, LOSPEC_CDN_BASE_URL).toString();
-}
- 
-function normalizePaletteExamples(
-  value: unknown,
-): LospecPaletteExample[] | null {
-  Iif (!Array.isArray(value)) {
-    return null;
-  }
- 
-  return value.flatMap((example) => {
-    Iif (!isRecord(example)) {
-      return [];
-    }
- 
-    const image = asNullableString(example.image);
-    if (!image) {
-      return [];
-    }
- 
-    return [
-      {
-        image: normalizePaletteExampleImage(image),
-        description: asNullableString(example.description),
-      },
-    ];
-  });
-}
- 
-function serializeJsonField(value: unknown): string | null {
-  Iif (value === undefined || value === null) {
-    return null;
-  }
- 
-  return JSON.stringify(value);
-}
- 
-function deserializeJsonField(value: string | null): unknown | null {
-  Iif (!value) {
-    return null;
-  }
- 
-  try {
-    return JSON.parse(value) as unknown;
-  } catch {
-    return value;
-  }
-}
- 
-export function mapPaletteToRow(
-  palette: LospecPaletteApiItem,
-): LospecPaletteRow {
-  return {
-    id: palette._id,
-    title: asNullableString(palette.title),
-    slug: asNullableString(palette.slug),
-    description: asNullableString(palette.description),
-    tags: serializeJsonField(palette.tags),
-    user: normalizePaletteUser(palette.user),
-    colors: serializeJsonField(palette.colors),
-    examples: serializeJsonField(normalizePaletteExamples(palette.examples)),
-    published_at: asNullableString(palette.publishedAt),
-  };
-}
- 
-export function mapRowToResponse(row: LospecPaletteRow): LospecPaletteResponse {
-  const user = normalizePaletteUser(deserializeJsonField(row.user));
-  const examples = normalizePaletteExamples(deserializeJsonField(row.examples));
- 
-  return {
-    id: row.id,
-    title: row.title,
-    slug: row.slug,
-    description: row.description,
-    tags: deserializeJsonField(row.tags),
-    user,
-    colors: deserializeJsonField(row.colors),
-    examples,
-    published_at: row.published_at,
-  };
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/routes/health.ts.html b/coverage/lcov-report/src/app/routes/health.ts.html deleted file mode 100644 index dcb99c2..0000000 --- a/coverage/lcov-report/src/app/routes/health.ts.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - Code coverage report for src/app/routes/health.ts - - - - - - - - - -
-
-

All files / src/app/routes health.ts

-
- -
- 100% - Statements - 1/1 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 1/1 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9  -  -  -  -  -  -2x -  - 
import type { Hono } from "hono";
- 
-import type { AppBindings } from "../../config/types";
-import { getHealth } from "../controllers/health";
- 
-export function registerHealthRoutes(app: Hono<AppBindings>): void {
-  app.get("/", getHealth);
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/routes/index.html b/coverage/lcov-report/src/app/routes/index.html deleted file mode 100644 index e8e6c98..0000000 --- a/coverage/lcov-report/src/app/routes/index.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - Code coverage report for src/app/routes - - - - - - - - - -
-
-

All files src/app/routes

-
- -
- 100% - Statements - 2/2 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 2/2 -
- - -
- 100% - Lines - 2/2 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
health.ts -
-
100%1/1100%0/0100%1/1100%1/1
lospec-palettes.ts -
-
100%1/1100%0/0100%1/1100%1/1
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/routes/lospec-palettes.ts.html b/coverage/lcov-report/src/app/routes/lospec-palettes.ts.html deleted file mode 100644 index bdf8093..0000000 --- a/coverage/lcov-report/src/app/routes/lospec-palettes.ts.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - Code coverage report for src/app/routes/lospec-palettes.ts - - - - - - - - - -
-
-

All files / src/app/routes lospec-palettes.ts

-
- -
- 100% - Statements - 1/1 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 1/1 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9  -  -  -  -  -  -2x -  - 
import type { Hono } from "hono";
- 
-import type { AppBindings } from "../../config/types";
-import { getLospecPalettes } from "../controllers/lospec-palettes";
- 
-export function registerLospecPaletteRoutes(app: Hono<AppBindings>): void {
-  app.get("/lospec_palettes", getLospecPalettes);
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/services/index.html b/coverage/lcov-report/src/app/services/index.html deleted file mode 100644 index 5127123..0000000 --- a/coverage/lcov-report/src/app/services/index.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - Code coverage report for src/app/services - - - - - - - - - -
-
-

All files src/app/services

-
- -
- 100% - Statements - 39/39 -
- - -
- 78.57% - Branches - 11/14 -
- - -
- 100% - Functions - 7/7 -
- - -
- 100% - Lines - 37/37 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
lospec-api.ts -
-
100%12/12100%4/4100%1/1100%12/12
lospec-palettes-repository.ts -
-
100%27/2770%7/10100%6/6100%25/25
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/services/lospec-api.ts.html b/coverage/lcov-report/src/app/services/lospec-api.ts.html deleted file mode 100644 index 91592dc..0000000 --- a/coverage/lcov-report/src/app/services/lospec-api.ts.html +++ /dev/null @@ -1,169 +0,0 @@ - - - - - - Code coverage report for src/app/services/lospec-api.ts - - - - - - - - - -
-
-

All files / src/app/services lospec-api.ts

-
- -
- 100% - Statements - 12/12 -
- - -
- 100% - Branches - 4/4 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 12/12 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29  -  -  -  -  -  -  -  -  -  -3x -3x -3x -3x -3x -  -3x -3x -1x -  -  -2x -2x -1x -  -  -1x -  - 
import { LOSPEC_PALETTE_LIST_URL } from "../../config/constants";
-import {
-  isLospecPaletteApiItem,
-  type LospecPaletteApiItem,
-} from "../models/lospec-palette";
- 
-export async function fetchLospecPalettePage(
-  page: number,
-  signal: AbortSignal,
-): Promise<LospecPaletteApiItem[]> {
-  const url = new URL(LOSPEC_PALETTE_LIST_URL);
-  url.searchParams.set("colorNumberFilterType", "any");
-  url.searchParams.set("page", String(page));
-  url.searchParams.set("tag", "");
-  url.searchParams.set("sortingType", "newest");
- 
-  const response = await fetch(url, { signal });
-  if (!response.ok) {
-    throw new Error(`Failed to fetch page ${page}: HTTP ${response.status}`);
-  }
- 
-  const payload = (await response.json()) as { palettes?: unknown };
-  if (!Array.isArray(payload.palettes)) {
-    return [];
-  }
- 
-  return payload.palettes.filter(isLospecPaletteApiItem);
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/app/services/lospec-palettes-repository.ts.html b/coverage/lcov-report/src/app/services/lospec-palettes-repository.ts.html deleted file mode 100644 index e3ed9eb..0000000 --- a/coverage/lcov-report/src/app/services/lospec-palettes-repository.ts.html +++ /dev/null @@ -1,427 +0,0 @@ - - - - - - Code coverage report for src/app/services/lospec-palettes-repository.ts - - - - - - - - - -
-
-

All files / src/app/services lospec-palettes-repository.ts

-
- -
- 100% - Statements - 27/27 -
- - -
- 70% - Branches - 7/10 -
- - -
- 100% - Functions - 6/6 -
- - -
- 100% - Lines - 25/25 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115  -  -  -  -  -  -  -  -  -  -  -  -2x -1x -  -  -2x -1x -  -  -1x -  -2x -  -  -  -  -  -  -2x -1x -  -  -1x -1x -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -1x -1x -  -  -  -  -  -  -1x -1x -  -1x -1x -1x -  -  -1x -1x -  -  -  -  -1x -  -  -  -1x -1x -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -1x -  - 
import {
-  mapPaletteToRow,
-  type ListLospecPalettesOptions,
-  type LospecPaletteApiItem,
-  type LospecPaletteRow,
-} from "../models/lospec-palette";
-import { LOSPEC_PALETTES_PAGE_SIZE } from "../../config/constants";
- 
-export async function getExistingPaletteIds(
-  db: D1Database,
-  ids: string[],
-): Promise<Set<string>> {
-  if (ids.length === 0) {
-    return new Set();
-  }
- 
-  const placeholders = ids.map(() => "?").join(", ");
-  const statement = db.prepare(
-    `SELECT id FROM lospec_palettes WHERE id IN (${placeholders})`,
-  );
-  const result = await statement.bind(...ids).all<{ id: string }>();
- 
-  return new Set(result.results.map((row) => row.id));
-}
- 
-export async function insertPalettes(
-  db: D1Database,
-  palettes: LospecPaletteApiItem[],
-): Promise<number> {
-  if (palettes.length === 0) {
-    return 0;
-  }
- 
-  const statements = palettes.map((palette) => {
-    const row = mapPaletteToRow(palette);
- 
-    return db
-      .prepare(
-        `INSERT OR IGNORE INTO lospec_palettes (
-          id,
-          title,
-          slug,
-          description,
-          tags,
-          user,
-          colors,
-          examples,
-          published_at
-        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
-      )
-      .bind(
-        row.id,
-        row.title,
-        row.slug,
-        row.description,
-        row.tags,
-        row.user,
-        row.colors,
-        row.examples,
-        row.published_at,
-      );
-  });
- 
-  await db.batch(statements);
-  return palettes.length;
-}
- 
-export async function listPalettes(
-  db: D1Database,
-  options: ListLospecPalettesOptions,
-): Promise<LospecPaletteRow[]> {
-  const whereClauses: string[] = [];
-  const bindings: Array<number | string> = [];
- 
-  Eif (options.search) {
-    whereClauses.push("LOWER(COALESCE(title, '')) LIKE ?");
-    bindings.push(`%${options.search.toLowerCase()}%`);
-  }
- 
-  Eif (options.tag) {
-    whereClauses.push(`EXISTS (
-      SELECT 1
-      FROM json_each(lospec_palettes.tags)
-      WHERE LOWER(CAST(json_each.value AS TEXT)) = ?
-    )`);
-    bindings.push(options.tag.toLowerCase());
-  }
- 
-  const whereSql =
-    whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
-  const offset = options.page * LOSPEC_PALETTES_PAGE_SIZE;
- 
-  const result = await db
-    .prepare(
-      `SELECT
-        id,
-        title,
-        slug,
-        description,
-        tags,
-        user,
-        colors,
-        examples,
-        published_at
-      FROM lospec_palettes
-      ${whereSql}
-      ORDER BY published_at DESC, id DESC
-      LIMIT ? OFFSET ?`,
-    )
-    .bind(...bindings, LOSPEC_PALETTES_PAGE_SIZE, offset)
-    .all<LospecPaletteRow>();
- 
-  return result.results;
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/config/constants.ts.html b/coverage/lcov-report/src/config/constants.ts.html deleted file mode 100644 index a32186a..0000000 --- a/coverage/lcov-report/src/config/constants.ts.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - Code coverage report for src/config/constants.ts - - - - - - - - - -
-
-

All files / src/config constants.ts

-
- -
- 100% - Statements - 5/5 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 0/0 -
- - -
- 100% - Lines - 5/5 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -143x -  -  -  -  -  -  -  -  -3x -3x -3x -3x - 
export const ALLOWED_ORIGINS = new Set([
-  "https://2dtiler.com",
-  "https://app.2dtiler.com",
-  "http://localhost:4321",
-  "http://127.0.0.1:4321",
-  "http://localhost:8787",
-  "http://127.0.0.1:8787",
-]);
- 
-export const INTERNAL_API_KEY_HEADER = "X-Internal-Api-Key";
-export const LOSPEC_PALETTE_LIST_URL = "https://lospec.com/palette-list/load";
-export const LOSPEC_CDN_BASE_URL = "https://cdn.lospec.com/";
-export const LOSPEC_PALETTES_PAGE_SIZE = 100;
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/config/index.html b/coverage/lcov-report/src/config/index.html deleted file mode 100644 index 27292e6..0000000 --- a/coverage/lcov-report/src/config/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for src/config - - - - - - - - - -
-
-

All files src/config

-
- -
- 100% - Statements - 5/5 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 0/0 -
- - -
- 100% - Lines - 5/5 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
constants.ts -
-
100%5/5100%0/0100%0/0100%5/5
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/index.html b/coverage/lcov-report/src/index.html deleted file mode 100644 index 6bd22b6..0000000 --- a/coverage/lcov-report/src/index.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - Code coverage report for src - - - - - - - - - -
-
-

All files src

-
- -
- 100% - Statements - 6/6 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 6/6 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
app.ts -
-
100%4/4100%0/0100%0/0100%4/4
index.ts -
-
100%2/2100%0/0100%1/1100%2/2
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/index.ts.html b/coverage/lcov-report/src/index.ts.html deleted file mode 100644 index 048c3a3..0000000 --- a/coverage/lcov-report/src/index.ts.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - Code coverage report for src/index.ts - - - - - - - - - -
-
-

All files / src index.ts

-
- -
- 100% - Statements - 2/2 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 2/2 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17  -  -  -  -  -  -  -  -  -  -  -  -1x -1x -  -  - 
import app from "./app";
-import { syncPalettes } from "./jobs/sync-palettes";
-import type { Env } from "./config/types";
- 
-export default {
-  fetch: app.fetch,
- 
-  async scheduled(
-    _event: ScheduledEvent,
-    env: Env,
-    ctx: ExecutionContext,
-  ): Promise<void> {
-    const signal = AbortSignal.timeout(15 * 60 * 1000);
-    ctx.waitUntil(syncPalettes(env, signal));
-  },
-};
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/jobs/index.html b/coverage/lcov-report/src/jobs/index.html deleted file mode 100644 index 5ade71f..0000000 --- a/coverage/lcov-report/src/jobs/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for src/jobs - - - - - - - - - -
-
-

All files src/jobs

-
- -
- 100% - Statements - 27/27 -
- - -
- 100% - Branches - 6/6 -
- - -
- 100% - Functions - 3/3 -
- - -
- 100% - Lines - 27/27 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
sync-palettes.ts -
-
100%27/27100%6/6100%3/3100%27/27
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/src/jobs/sync-palettes.ts.html b/coverage/lcov-report/src/jobs/sync-palettes.ts.html deleted file mode 100644 index 3e232be..0000000 --- a/coverage/lcov-report/src/jobs/sync-palettes.ts.html +++ /dev/null @@ -1,271 +0,0 @@ - - - - - - Code coverage report for src/jobs/sync-palettes.ts - - - - - - - - - -
-
-

All files / src/jobs sync-palettes.ts

-
- -
- 100% - Statements - 27/27 -
- - -
- 100% - Branches - 6/6 -
- - -
- 100% - Functions - 3/3 -
- - -
- 100% - Lines - 27/27 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63  -  -  -  -  -  -  -  -  -  -  -4x -4x -4x -  -4x -5x -5x -3x -1x -1x -  -  -2x -  -2x -  -2x -2x -  -2x -  -2x -2x -  -2x -  -  -  -2x -1x -  -  -1x -  -  -1x -  -2x -1x -1x -  -  -1x -1x -  -  -  -3x -  -  -  - 
import type { Env } from "../config/types";
-import { fetchLospecPalettePage } from "../app/services/lospec-api";
-import {
-  getExistingPaletteIds,
-  insertPalettes,
-} from "../app/services/lospec-palettes-repository";
- 
-export async function syncPalettes(
-  env: Env,
-  signal: AbortSignal,
-): Promise<void> {
-  let page = 0;
-  let pagesProcessed = 0;
-  let totalInserted = 0;
- 
-  while (!signal.aborted) {
-    try {
-      const palettes = await fetchLospecPalettePage(page, signal);
-      if (palettes.length === 0) {
-        console.log(`Page ${page}: no palettes returned; stopping pagination`);
-        break;
-      }
- 
-      const existingIds = await getExistingPaletteIds(
-        env.DB,
-        palettes.map((palette) => palette._id),
-      );
-      const unseenPalettes = palettes.filter(
-        (palette) => !existingIds.has(palette._id),
-      );
-      const inserted = await insertPalettes(env.DB, unseenPalettes);
- 
-      pagesProcessed++;
-      totalInserted += inserted;
- 
-      console.log(
-        `Page ${page}: fetched ${palettes.length}, inserted ${inserted}, existing ${existingIds.size}`,
-      );
- 
-      if (existingIds.size > 0) {
-        console.log(
-          `Page ${page}: encountered existing palette IDs; stopping pagination`,
-        );
-        break;
-      }
- 
-      page++;
-    } catch (error) {
-      if (signal.aborted) {
-        console.warn("Palette sync timed out; stopping early");
-        break;
-      }
- 
-      console.error(`Page ${page}: failed to sync palettes`, error);
-      return;
-    }
-  }
- 
-  console.log(
-    `Palette sync complete: inserted ${totalInserted} palettes across ${pagesProcessed} page(s)`,
-  );
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info deleted file mode 100644 index 8b60543..0000000 --- a/coverage/lcov.info +++ /dev/null @@ -1,385 +0,0 @@ -TN: -SF:src/app.ts -FNF:0 -FNH:0 -DA:8,2 -DA:10,2 -DA:12,2 -DA:13,2 -LF:4 -LH:4 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/index.ts -FN:8,(anonymous_0) -FNF:1 -FNH:1 -FNDA:1,(anonymous_0) -DA:13,1 -DA:14,1 -LF:2 -LH:2 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/app/controllers/health.ts -FN:5,getHealth -FNF:1 -FNH:1 -FNDA:3,getHealth -DA:6,3 -LF:1 -LH:1 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/app/controllers/lospec-palettes.ts -FN:11,parsePage -FN:23,normalizeQueryValue -FN:28,parseListLospecPalettesOptions -FN:43,getLospecPalettes -FNF:4 -FNH:4 -FNDA:3,parsePage -FNDA:4,normalizeQueryValue -FNDA:3,parseListLospecPalettesOptions -FNDA:3,getLospecPalettes -DA:12,3 -DA:13,1 -DA:16,2 -DA:17,1 -DA:20,1 -DA:24,4 -DA:25,4 -DA:31,3 -DA:32,3 -DA:33,1 -DA:36,2 -DA:46,3 -DA:47,3 -DA:48,1 -DA:56,2 -DA:57,2 -DA:58,2 -DA:59,1 -DA:62,1 -DA:63,1 -LF:20 -LH:20 -BRDA:12,0,0,1 -BRDA:12,0,1,2 -BRDA:16,1,0,1 -BRDA:16,1,1,1 -BRDA:25,2,0,2 -BRDA:25,2,1,2 -BRDA:32,3,0,1 -BRDA:32,3,1,2 -BRDA:47,4,0,1 -BRDA:47,4,1,2 -BRDA:56,5,0,2 -BRDA:56,5,1,2 -BRDA:58,6,0,1 -BRDA:58,6,1,1 -BRF:14 -BRH:14 -end_of_record -TN: -SF:src/app/middleware/cors.ts -FN:9,applyCorsHeaders -FN:16,(anonymous_1) -FNF:2 -FNH:2 -FNDA:2,applyCorsHeaders -FNDA:8,(anonymous_1) -DA:10,2 -DA:11,2 -DA:12,2 -DA:13,2 -DA:16,2 -DA:20,8 -DA:21,8 -DA:23,8 -DA:24,1 -DA:25,1 -DA:28,7 -DA:29,4 -DA:30,4 -DA:33,3 -DA:34,1 -DA:37,2 -DA:38,1 -DA:39,1 -DA:40,1 -DA:43,1 -DA:44,1 -LF:21 -LH:21 -BRDA:23,0,0,1 -BRDA:23,0,1,7 -BRDA:23,1,0,8 -BRDA:23,1,1,8 -BRDA:28,2,0,4 -BRDA:28,2,1,3 -BRDA:33,3,0,1 -BRDA:33,3,1,2 -BRDA:37,4,0,1 -BRDA:37,4,1,1 -BRF:10 -BRH:10 -end_of_record -TN: -SF:src/app/models/lospec-palette.ts -FN:50,isRecord -FN:54,isLospecPaletteApiItem -FN:60,asNullableString -FN:64,normalizePaletteUser -FN:76,normalizePaletteExampleImage -FN:80,normalizePaletteExamples -FN:87,(anonymous_6) -FN:106,serializeJsonField -FN:114,deserializeJsonField -FN:126,mapPaletteToRow -FN:142,mapRowToResponse -FNF:11 -FNH:11 -FNDA:11,isRecord -FNDA:4,isLospecPaletteApiItem -FNDA:14,asNullableString -FNDA:3,normalizePaletteUser -FNDA:3,normalizePaletteExampleImage -FNDA:3,normalizePaletteExamples -FNDA:4,(anonymous_6) -FNDA:3,serializeJsonField -FNDA:8,deserializeJsonField -FNDA:1,mapPaletteToRow -FNDA:2,mapRowToResponse -DA:51,11 -DA:57,4 -DA:61,14 -DA:65,3 -DA:66,0 -DA:69,3 -DA:70,0 -DA:73,3 -DA:77,3 -DA:83,3 -DA:84,0 -DA:87,3 -DA:88,4 -DA:89,0 -DA:92,4 -DA:93,4 -DA:94,1 -DA:97,3 -DA:107,3 -DA:108,0 -DA:111,3 -DA:115,8 -DA:116,0 -DA:119,8 -DA:120,8 -DA:122,1 -DA:129,1 -DA:143,2 -DA:144,2 -DA:146,2 -LF:30 -LH:24 -BRDA:51,0,0,11 -BRDA:51,0,1,11 -BRDA:57,1,0,4 -BRDA:57,1,1,4 -BRDA:61,2,0,13 -BRDA:61,2,1,1 -BRDA:65,3,0,0 -BRDA:65,3,1,3 -BRDA:69,4,0,0 -BRDA:69,4,1,3 -BRDA:83,5,0,0 -BRDA:83,5,1,3 -BRDA:88,6,0,0 -BRDA:88,6,1,4 -BRDA:93,7,0,1 -BRDA:93,7,1,3 -BRDA:107,8,0,0 -BRDA:107,8,1,3 -BRDA:107,9,0,3 -BRDA:107,9,1,3 -BRDA:115,10,0,0 -BRDA:115,10,1,8 -BRF:22 -BRH:16 -end_of_record -TN: -SF:src/app/routes/health.ts -FN:6,registerHealthRoutes -FNF:1 -FNH:1 -FNDA:2,registerHealthRoutes -DA:7,2 -LF:1 -LH:1 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/app/routes/lospec-palettes.ts -FN:6,registerLospecPaletteRoutes -FNF:1 -FNH:1 -FNDA:2,registerLospecPaletteRoutes -DA:7,2 -LF:1 -LH:1 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/app/services/lospec-api.ts -FN:7,fetchLospecPalettePage -FNF:1 -FNH:1 -FNDA:3,fetchLospecPalettePage -DA:11,3 -DA:12,3 -DA:13,3 -DA:14,3 -DA:15,3 -DA:17,3 -DA:18,3 -DA:19,1 -DA:22,2 -DA:23,2 -DA:24,1 -DA:27,1 -LF:12 -LH:12 -BRDA:18,0,0,1 -BRDA:18,0,1,2 -BRDA:23,1,0,1 -BRDA:23,1,1,1 -BRF:4 -BRH:4 -end_of_record -TN: -SF:src/app/services/lospec-palettes-repository.ts -FN:9,getExistingPaletteIds -FN:17,(anonymous_1) -FN:23,(anonymous_2) -FN:26,insertPalettes -FN:34,(anonymous_4) -FN:68,listPalettes -FNF:6 -FNH:6 -FNDA:2,getExistingPaletteIds -FNDA:2,(anonymous_1) -FNDA:2,(anonymous_2) -FNDA:2,insertPalettes -FNDA:1,(anonymous_4) -FNDA:1,listPalettes -DA:13,2 -DA:14,1 -DA:17,2 -DA:18,1 -DA:21,1 -DA:23,2 -DA:30,2 -DA:31,1 -DA:34,1 -DA:35,1 -DA:37,1 -DA:64,1 -DA:65,1 -DA:72,1 -DA:73,1 -DA:75,1 -DA:76,1 -DA:77,1 -DA:80,1 -DA:81,1 -DA:86,1 -DA:90,1 -DA:91,1 -DA:93,1 -DA:113,1 -LF:25 -LH:25 -BRDA:13,0,0,1 -BRDA:13,0,1,1 -BRDA:30,1,0,1 -BRDA:30,1,1,1 -BRDA:75,2,0,1 -BRDA:75,2,1,0 -BRDA:80,3,0,1 -BRDA:80,3,1,0 -BRDA:90,4,0,1 -BRDA:90,4,1,0 -BRF:10 -BRH:7 -end_of_record -TN: -SF:src/config/constants.ts -FNF:0 -FNH:0 -DA:1,3 -DA:10,3 -DA:11,3 -DA:12,3 -DA:13,3 -LF:5 -LH:5 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/jobs/sync-palettes.ts -FN:8,syncPalettes -FN:26,(anonymous_1) -FN:28,(anonymous_2) -FNF:3 -FNH:3 -FNDA:4,syncPalettes -FNDA:2,(anonymous_1) -FNDA:2,(anonymous_2) -DA:12,4 -DA:13,4 -DA:14,4 -DA:16,4 -DA:17,5 -DA:18,5 -DA:19,3 -DA:20,1 -DA:21,1 -DA:24,2 -DA:26,2 -DA:28,2 -DA:29,2 -DA:31,2 -DA:33,2 -DA:34,2 -DA:36,2 -DA:40,2 -DA:41,1 -DA:44,1 -DA:47,1 -DA:49,2 -DA:50,1 -DA:51,1 -DA:54,1 -DA:55,1 -DA:59,3 -LF:27 -LH:27 -BRDA:19,0,0,1 -BRDA:19,0,1,2 -BRDA:40,1,0,1 -BRDA:40,1,1,1 -BRDA:49,2,0,1 -BRDA:49,2,1,1 -BRF:6 -BRH:6 -end_of_record From d8cb0f7e75c21599af965b16fa3c98afdd1a5016 Mon Sep 17 00:00:00 2001 From: Werner Bihl Date: Fri, 10 Apr 2026 15:11:14 +0200 Subject: [PATCH 6/7] Update rate limiting to 10 requests per minute and adjust error message for exceeded limits --- .github/workflows/test-dev.yml | 4 +--- src/app/controllers/lospec-palettes.ts | 5 ++++- wrangler.toml | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index 8e4f658..333d1c1 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -1,4 +1,4 @@ -name: Deploy Dev +name: Test Dev on: pull_request: @@ -10,8 +10,6 @@ on: # Allow this job to clone the repo and create a page deployment permissions: contents: read - pages: write - id-token: write jobs: test-api: diff --git a/src/app/controllers/lospec-palettes.ts b/src/app/controllers/lospec-palettes.ts index f45a690..7d461ee 100644 --- a/src/app/controllers/lospec-palettes.ts +++ b/src/app/controllers/lospec-palettes.ts @@ -56,7 +56,10 @@ export async function getLospecPalettes( const ip = c.req.raw.headers.get("CF-Connecting-IP") ?? "unknown"; const { success } = await c.env.RATE_LIMITER.limit({ key: ip }); if (!success) { - return c.json({ error: "Rate limit exceeded. Try again in an hour." }, 429); + return c.json( + { error: "Rate limit exceeded. Try again in a minute." }, + 429, + ); } const palettes = await listPalettes(c.env.DB, options); diff --git a/wrangler.toml b/wrangler.toml index 5bdc745..91be431 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -15,7 +15,7 @@ database_id = "bd9d2b49-65a8-4684-9911-38c6095c6f0b" # Run daily at 14:00 UTC crons = ["0 14 * * *"] -# Rate limiting: 10 requests per hour per IP +# Rate limiting: 10 requests per minute per IP [[unsafe.bindings]] name = "RATE_LIMITER" type = "ratelimit" From b4641ddfb00e4f86bd121f45901ed521326eaf5b Mon Sep 17 00:00:00 2001 From: Werner Bihl Date: Fri, 10 Apr 2026 15:12:20 +0200 Subject: [PATCH 7/7] Update rate limit exceeded error message to reflect a one-minute wait --- tests/app.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/app.test.ts b/tests/app.test.ts index eb29321..e646b60 100644 --- a/tests/app.test.ts +++ b/tests/app.test.ts @@ -140,7 +140,7 @@ describe("app", () => { expect(response.status).toBe(429); expect(await response.json()).toEqual({ - error: "Rate limit exceeded. Try again in an hour.", + error: "Rate limit exceeded. Try again in a minute.", }); });