search for standard sites pub-search.waow.tech
search zig blog atproto

leaflet search: full-text search for leaflet documents

- zig backend with fts5 search via turso
- tap service for firehose consumption and backfill
- cloudflare pages frontend with shareable urls

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+2922
+7
.gitignore
··· 1 + node_modules/ 2 + .wrangler/ 3 + dist/ 4 + .env 5 + *.db 6 + .zig-cache/ 7 + zig-out/
+33
backend/Dockerfile
··· 1 + # build stage 2 + FROM debian:bookworm-slim AS builder 3 + 4 + RUN apt-get update && apt-get install -y --no-install-recommends \ 5 + ca-certificates \ 6 + curl \ 7 + xz-utils \ 8 + && rm -rf /var/lib/apt/lists/* 9 + 10 + # install zig 0.15.2 11 + RUN curl -L https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz | tar -xJ -C /usr/local \ 12 + && ln -s /usr/local/zig-x86_64-linux-0.15.2/zig /usr/local/bin/zig 13 + 14 + WORKDIR /app 15 + COPY build.zig build.zig.zon ./ 16 + COPY src ./src 17 + 18 + RUN zig build -Doptimize=ReleaseSafe 19 + 20 + # runtime stage 21 + FROM debian:bookworm-slim 22 + 23 + RUN apt-get update && apt-get install -y --no-install-recommends \ 24 + ca-certificates \ 25 + && rm -rf /var/lib/apt/lists/* \ 26 + && echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf 27 + 28 + WORKDIR /app 29 + COPY --from=builder /app/zig-out/bin/leaflet-search . 30 + 31 + EXPOSE 3000 32 + 33 + CMD ["./leaflet-search"]
+34
backend/build.zig
··· 1 + const std = @import("std"); 2 + 3 + pub fn build(b: *std.Build) void { 4 + const target = b.standardTargetOptions(.{}); 5 + const optimize = b.standardOptimizeOption(.{}); 6 + 7 + const websocket = b.dependency("websocket", .{ 8 + .target = target, 9 + .optimize = optimize, 10 + }); 11 + 12 + const exe = b.addExecutable(.{ 13 + .name = "leaflet-search", 14 + .root_module = b.createModule(.{ 15 + .root_source_file = b.path("src/main.zig"), 16 + .target = target, 17 + .optimize = optimize, 18 + .imports = &.{ 19 + .{ .name = "websocket", .module = websocket.module("websocket") }, 20 + }, 21 + }), 22 + }); 23 + 24 + b.installArtifact(exe); 25 + 26 + const run_cmd = b.addRunArtifact(exe); 27 + run_cmd.step.dependOn(b.getInstallStep()); 28 + if (b.args) |args| { 29 + run_cmd.addArgs(args); 30 + } 31 + 32 + const run_step = b.step("run", "Run the server"); 33 + run_step.dependOn(&run_cmd.step); 34 + }
+17
backend/build.zig.zon
··· 1 + .{ 2 + .name = .leaflet_search, 3 + .version = "0.0.1", 4 + .fingerprint = 0x4a432eb7171f22eb, 5 + .minimum_zig_version = "0.15.0", 6 + .dependencies = .{ 7 + .websocket = .{ 8 + .url = "https://github.com/karlseguin/websocket.zig/archive/refs/heads/master.tar.gz", 9 + .hash = "websocket-0.1.0-ZPISdRNzAwAGszh62EpRtoQxu8wb1MSMVI6Ow0o2dmyJ", 10 + }, 11 + }, 12 + .paths = .{ 13 + "build.zig", 14 + "build.zig.zon", 15 + "src", 16 + }, 17 + }
+21
backend/fly.toml
··· 1 + app = 'leaflet-search-backend' 2 + primary_region = 'ewr' 3 + 4 + [build] 5 + 6 + [env] 7 + TAP_HOST = 'leaflet-search-tap.internal' 8 + TAP_PORT = '2480' 9 + 10 + [http_service] 11 + internal_port = 3000 12 + force_https = true 13 + auto_stop_machines = 'stop' 14 + auto_start_machines = true 15 + min_machines_running = 1 16 + processes = ['app'] 17 + 18 + [[vm]] 19 + memory = '256mb' 20 + cpu_kind = 'shared' 21 + cpus = 1
+19
backend/justfile
··· 1 + # leaflet-search backend 2 + 3 + build: 4 + zig build 5 + 6 + run: 7 + zig build run 8 + 9 + deploy: 10 + fly deploy --app leaflet-search-backend 11 + 12 + status: 13 + fly status --app leaflet-search-backend 14 + 15 + logs: 16 + fly logs --app leaflet-search-backend 17 + 18 + ssh: 19 + fly ssh console --app leaflet-search-backend
+356
backend/src/db.zig
··· 1 + const std = @import("std"); 2 + const mem = std.mem; 3 + const Thread = std.Thread; 4 + const Allocator = mem.Allocator; 5 + const json = std.json; 6 + const http = std.http; 7 + const Io = std.Io; 8 + 9 + pub var turso_url: []const u8 = undefined; 10 + pub var turso_token: []const u8 = undefined; 11 + pub var mutex: Thread.Mutex = .{}; 12 + 13 + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; 14 + 15 + pub fn init() !void { 16 + turso_url = std.posix.getenv("TURSO_URL") orelse { 17 + std.debug.print("TURSO_URL not set\n", .{}); 18 + return error.MissingEnv; 19 + }; 20 + turso_token = std.posix.getenv("TURSO_TOKEN") orelse { 21 + std.debug.print("TURSO_TOKEN not set\n", .{}); 22 + return error.MissingEnv; 23 + }; 24 + 25 + std.debug.print("using turso database: {s}\n", .{turso_url}); 26 + try initSchema(); 27 + } 28 + 29 + pub fn close() void {} 30 + 31 + fn initSchema() !void { 32 + _ = try execSqlNoArgs( 33 + \\CREATE TABLE IF NOT EXISTS documents ( 34 + \\ uri TEXT PRIMARY KEY, 35 + \\ did TEXT NOT NULL, 36 + \\ rkey TEXT NOT NULL, 37 + \\ title TEXT NOT NULL, 38 + \\ content TEXT NOT NULL, 39 + \\ created_at TEXT 40 + \\) 41 + ); 42 + 43 + _ = try execSqlNoArgs( 44 + \\CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5( 45 + \\ uri UNINDEXED, 46 + \\ title, 47 + \\ content 48 + \\) 49 + ); 50 + 51 + _ = try execSqlNoArgs( 52 + \\CREATE TABLE IF NOT EXISTS publications ( 53 + \\ uri TEXT PRIMARY KEY, 54 + \\ did TEXT NOT NULL, 55 + \\ rkey TEXT NOT NULL, 56 + \\ name TEXT NOT NULL, 57 + \\ description TEXT 58 + \\) 59 + ); 60 + 61 + std.debug.print("turso schema initialized with FTS5\n", .{}); 62 + } 63 + 64 + pub fn insertDocument(uri: []const u8, did: []const u8, rkey: []const u8, title: []const u8, content: []const u8, created_at: ?[]const u8) !void { 65 + _ = try execSqlWithArgs( 66 + "INSERT OR REPLACE INTO documents (uri, did, rkey, title, content, created_at) VALUES (?, ?, ?, ?, ?, ?)", 67 + &[_][]const u8{ uri, did, rkey, title, content, created_at orelse "" }, 68 + ); 69 + 70 + // delete from fts first (ignore errors) 71 + _ = execSqlWithArgs("DELETE FROM documents_fts WHERE uri = ?", &[_][]const u8{uri}) catch {}; 72 + 73 + _ = execSqlWithArgs( 74 + "INSERT INTO documents_fts (uri, title, content) VALUES (?, ?, ?)", 75 + &[_][]const u8{ uri, title, content }, 76 + ) catch |err| { 77 + std.debug.print("insert FTS error: {}\n", .{err}); 78 + }; 79 + } 80 + 81 + pub fn insertPublication(uri: []const u8, did: []const u8, rkey: []const u8, name: []const u8, description: ?[]const u8) !void { 82 + _ = try execSqlWithArgs( 83 + "INSERT OR REPLACE INTO publications (uri, did, rkey, name, description) VALUES (?, ?, ?, ?, ?)", 84 + &[_][]const u8{ uri, did, rkey, name, description orelse "" }, 85 + ); 86 + } 87 + 88 + pub fn deleteDocument(uri: []const u8) void { 89 + _ = execSqlWithArgs("DELETE FROM documents WHERE uri = ?", &[_][]const u8{uri}) catch {}; 90 + _ = execSqlWithArgs("DELETE FROM documents_fts WHERE uri = ?", &[_][]const u8{uri}) catch {}; 91 + } 92 + 93 + pub fn deletePublication(uri: []const u8) void { 94 + _ = execSqlWithArgs("DELETE FROM publications WHERE uri = ?", &[_][]const u8{uri}) catch {}; 95 + } 96 + 97 + pub fn searchDocuments(alloc: Allocator, query: []const u8) !std.ArrayList(u8) { 98 + var response: std.ArrayList(u8) = .{}; 99 + try response.appendSlice(alloc, "["); 100 + 101 + const temp_alloc = gpa.allocator(); 102 + 103 + const result = execSqlWithArgs( 104 + "SELECT f.uri, d.did, d.title, snippet(documents_fts, 2, '<mark>', '</mark>', '...', 32) as snippet, d.created_at FROM documents_fts f JOIN documents d ON f.uri = d.uri WHERE documents_fts MATCH ? ORDER BY rank LIMIT 50", 105 + &[_][]const u8{query}, 106 + ) catch { 107 + try response.appendSlice(alloc, "]"); 108 + return response; 109 + }; 110 + defer temp_alloc.free(result); 111 + 112 + const parsed = json.parseFromSlice(json.Value, temp_alloc, result, .{}) catch { 113 + try response.appendSlice(alloc, "]"); 114 + return response; 115 + }; 116 + defer parsed.deinit(); 117 + 118 + const results = parsed.value.object.get("results") orelse { 119 + try response.appendSlice(alloc, "]"); 120 + return response; 121 + }; 122 + 123 + if (results != .array or results.array.items.len == 0) { 124 + try response.appendSlice(alloc, "]"); 125 + return response; 126 + } 127 + 128 + const first_result = results.array.items[0]; 129 + if (first_result != .object) { 130 + try response.appendSlice(alloc, "]"); 131 + return response; 132 + } 133 + 134 + const resp_obj = first_result.object.get("response") orelse { 135 + try response.appendSlice(alloc, "]"); 136 + return response; 137 + }; 138 + 139 + if (resp_obj != .object) { 140 + try response.appendSlice(alloc, "]"); 141 + return response; 142 + } 143 + 144 + const result_obj = resp_obj.object.get("result") orelse { 145 + try response.appendSlice(alloc, "]"); 146 + return response; 147 + }; 148 + 149 + if (result_obj != .object) { 150 + try response.appendSlice(alloc, "]"); 151 + return response; 152 + } 153 + 154 + const rows = result_obj.object.get("rows") orelse { 155 + try response.appendSlice(alloc, "]"); 156 + return response; 157 + }; 158 + 159 + if (rows != .array) { 160 + try response.appendSlice(alloc, "]"); 161 + return response; 162 + } 163 + 164 + var first = true; 165 + for (rows.array.items) |row| { 166 + if (row != .array) continue; 167 + const cols = row.array.items; 168 + if (cols.len < 5) continue; 169 + 170 + if (!first) try response.appendSlice(alloc, ","); 171 + first = false; 172 + 173 + const uri = getTextValue(cols[0]); 174 + const did = getTextValue(cols[1]); 175 + const title = getTextValue(cols[2]); 176 + const snippet = getTextValue(cols[3]); 177 + const created_at = getTextValue(cols[4]); 178 + 179 + try response.appendSlice(alloc, "{\"uri\":\""); 180 + try appendEscaped(alloc, &response, uri); 181 + try response.appendSlice(alloc, "\",\"did\":\""); 182 + try appendEscaped(alloc, &response, did); 183 + try response.appendSlice(alloc, "\",\"title\":\""); 184 + try appendEscaped(alloc, &response, title); 185 + try response.appendSlice(alloc, "\",\"snippet\":\""); 186 + try appendEscaped(alloc, &response, snippet); 187 + try response.appendSlice(alloc, "\",\"createdAt\":\""); 188 + try appendEscaped(alloc, &response, created_at); 189 + try response.appendSlice(alloc, "\"}"); 190 + } 191 + 192 + try response.appendSlice(alloc, "]"); 193 + return response; 194 + } 195 + 196 + fn getTextValue(val: json.Value) []const u8 { 197 + return switch (val) { 198 + .string => |s| s, 199 + .object => |obj| if (obj.get("value")) |v| (if (v == .string) v.string else "") else "", 200 + else => "", 201 + }; 202 + } 203 + 204 + fn appendEscaped(alloc: Allocator, list: *std.ArrayList(u8), s: []const u8) !void { 205 + for (s) |c| { 206 + switch (c) { 207 + '"' => try list.appendSlice(alloc, "\\\""), 208 + '\\' => try list.appendSlice(alloc, "\\\\"), 209 + '\n' => try list.appendSlice(alloc, "\\n"), 210 + '\r' => try list.appendSlice(alloc, "\\r"), 211 + '\t' => try list.appendSlice(alloc, "\\t"), 212 + else => try list.append(alloc, c), 213 + } 214 + } 215 + } 216 + 217 + fn execSqlNoArgs(sql: []const u8) ![]const u8 { 218 + return execSqlWithArgs(sql, &[_][]const u8{}); 219 + } 220 + 221 + fn execSqlWithArgs(sql: []const u8, args: []const []const u8) ![]const u8 { 222 + mutex.lock(); 223 + defer mutex.unlock(); 224 + 225 + const alloc = gpa.allocator(); 226 + 227 + // libsql:// -> https:// 228 + var url_buf: [512]u8 = undefined; 229 + const url = std.fmt.bufPrint(&url_buf, "https://{s}/v2/pipeline", .{ 230 + if (mem.startsWith(u8, turso_url, "libsql://")) 231 + turso_url[9..] 232 + else 233 + turso_url, 234 + }) catch return error.UrlTooLong; 235 + 236 + // build request body with parameterized args 237 + var body: std.ArrayList(u8) = .{}; 238 + defer body.deinit(alloc); 239 + 240 + try body.appendSlice(alloc, "{\"requests\":[{\"type\":\"execute\",\"stmt\":{\"sql\":\""); 241 + for (sql) |c| { 242 + switch (c) { 243 + '"' => try body.appendSlice(alloc, "\\\""), 244 + '\\' => try body.appendSlice(alloc, "\\\\"), 245 + '\n' => try body.appendSlice(alloc, "\\n"), 246 + '\r' => try body.appendSlice(alloc, "\\r"), 247 + '\t' => try body.appendSlice(alloc, "\\t"), 248 + else => try body.append(alloc, c), 249 + } 250 + } 251 + try body.appendSlice(alloc, "\""); 252 + 253 + // add args array if we have any 254 + if (args.len > 0) { 255 + try body.appendSlice(alloc, ",\"args\":["); 256 + for (args, 0..) |arg, i| { 257 + if (i > 0) try body.appendSlice(alloc, ","); 258 + try body.appendSlice(alloc, "{\"type\":\"text\",\"value\":\""); 259 + for (arg) |c| { 260 + switch (c) { 261 + '"' => try body.appendSlice(alloc, "\\\""), 262 + '\\' => try body.appendSlice(alloc, "\\\\"), 263 + '\n' => try body.appendSlice(alloc, "\\n"), 264 + '\r' => try body.appendSlice(alloc, "\\r"), 265 + '\t' => try body.appendSlice(alloc, "\\t"), 266 + else => try body.append(alloc, c), 267 + } 268 + } 269 + try body.appendSlice(alloc, "\"}"); 270 + } 271 + try body.appendSlice(alloc, "]"); 272 + } 273 + 274 + try body.appendSlice(alloc, "}},{\"type\":\"close\"}]}"); 275 + 276 + var auth_buf: [512]u8 = undefined; 277 + const auth_header = std.fmt.bufPrint(&auth_buf, "Bearer {s}", .{turso_token}) catch return error.AuthTooLong; 278 + 279 + var client: http.Client = .{ .allocator = alloc }; 280 + defer client.deinit(); 281 + 282 + var aw: Io.Writer.Allocating = .init(alloc); 283 + defer aw.deinit(); 284 + 285 + const result = client.fetch(.{ 286 + .location = .{ .url = url }, 287 + .method = .POST, 288 + .headers = .{ 289 + .content_type = .{ .override = "application/json" }, 290 + .authorization = .{ .override = auth_header }, 291 + }, 292 + .payload = body.items, 293 + .response_writer = &aw.writer, 294 + }) catch |err| { 295 + std.debug.print("turso request failed: {}\n", .{err}); 296 + return error.HttpError; 297 + }; 298 + 299 + if (result.status != .ok) { 300 + std.debug.print("turso error: {}\n", .{result.status}); 301 + return error.TursoError; 302 + } 303 + 304 + return try aw.toOwnedSlice(); 305 + } 306 + 307 + pub fn getStats() struct { documents: i64, publications: i64 } { 308 + const doc_result = execSqlNoArgs("SELECT COUNT(*) FROM documents") catch return .{ .documents = 0, .publications = 0 }; 309 + defer gpa.allocator().free(doc_result); 310 + 311 + const pub_result = execSqlNoArgs("SELECT COUNT(*) FROM publications") catch return .{ .documents = 0, .publications = 0 }; 312 + defer gpa.allocator().free(pub_result); 313 + 314 + const doc_count = parseCount(doc_result); 315 + const pub_count = parseCount(pub_result); 316 + 317 + return .{ .documents = doc_count, .publications = pub_count }; 318 + } 319 + 320 + fn parseCount(result: []const u8) i64 { 321 + const alloc = gpa.allocator(); 322 + const parsed = json.parseFromSlice(json.Value, alloc, result, .{}) catch return 0; 323 + defer parsed.deinit(); 324 + 325 + const results = parsed.value.object.get("results") orelse return 0; 326 + if (results != .array or results.array.items.len == 0) return 0; 327 + 328 + const first = results.array.items[0]; 329 + if (first != .object) return 0; 330 + 331 + const resp = first.object.get("response") orelse return 0; 332 + if (resp != .object) return 0; 333 + 334 + const res = resp.object.get("result") orelse return 0; 335 + if (res != .object) return 0; 336 + 337 + const rows = res.object.get("rows") orelse return 0; 338 + if (rows != .array or rows.array.items.len == 0) return 0; 339 + 340 + const row = rows.array.items[0]; 341 + if (row != .array or row.array.items.len == 0) return 0; 342 + 343 + const val = row.array.items[0]; 344 + return switch (val) { 345 + .integer => |i| i, 346 + .object => |obj| blk: { 347 + const v = obj.get("value") orelse break :blk 0; 348 + break :blk switch (v) { 349 + .integer => |i| i, 350 + .string => |s| std.fmt.parseInt(i64, s, 10) catch 0, 351 + else => 0, 352 + }; 353 + }, 354 + else => 0, 355 + }; 356 + }
+131
backend/src/http.zig
··· 1 + const std = @import("std"); 2 + const net = std.net; 3 + const http = std.http; 4 + const mem = std.mem; 5 + const db = @import("db.zig"); 6 + 7 + pub fn handleConnection(conn: net.Server.Connection) void { 8 + defer conn.stream.close(); 9 + 10 + var read_buffer: [8192]u8 = undefined; 11 + var write_buffer: [8192]u8 = undefined; 12 + 13 + var reader = conn.stream.reader(&read_buffer); 14 + var writer = conn.stream.writer(&write_buffer); 15 + 16 + var server = http.Server.init(reader.interface(), &writer.interface); 17 + 18 + while (true) { 19 + var request = server.receiveHead() catch |err| { 20 + if (err != error.HttpConnectionClosing and err != error.EndOfStream) { 21 + std.debug.print("http receive error: {}\n", .{err}); 22 + } 23 + return; 24 + }; 25 + handleRequest(&server, &request) catch |err| { 26 + std.debug.print("request error: {}\n", .{err}); 27 + return; 28 + }; 29 + if (!request.head.keep_alive) return; 30 + } 31 + } 32 + 33 + fn handleRequest(server: *http.Server, request: *http.Server.Request) !void { 34 + _ = server; 35 + const target = request.head.target; 36 + 37 + // cors preflight 38 + if (request.head.method == .OPTIONS) { 39 + try sendCorsHeaders(request, ""); 40 + return; 41 + } 42 + 43 + if (mem.startsWith(u8, target, "/search")) { 44 + try handleSearch(request, target); 45 + } else if (mem.eql(u8, target, "/stats")) { 46 + try handleStats(request); 47 + } else if (mem.eql(u8, target, "/health")) { 48 + try sendJson(request, "{\"status\":\"ok\"}"); 49 + } else { 50 + try sendNotFound(request); 51 + } 52 + } 53 + 54 + fn handleSearch(request: *http.Server.Request, target: []const u8) !void { 55 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 56 + defer arena.deinit(); 57 + const alloc = arena.allocator(); 58 + 59 + // parse query param: /search?q=something 60 + const query = blk: { 61 + if (mem.indexOf(u8, target, "?q=")) |idx| { 62 + const encoded = target[idx + 3 ..]; 63 + // find end of query param (next & or end of string) 64 + const end = mem.indexOf(u8, encoded, "&") orelse encoded.len; 65 + const query_encoded = encoded[0..end]; 66 + // decode percent-encoding 67 + const buf = try alloc.dupe(u8, query_encoded); 68 + break :blk std.Uri.percentDecodeInPlace(buf); 69 + } 70 + break :blk ""; 71 + }; 72 + 73 + if (query.len == 0) { 74 + try sendJson(request, "{\"error\":\"missing q parameter\"}"); 75 + return; 76 + } 77 + 78 + // perform FTS search 79 + var results = try db.searchDocuments(alloc, query); 80 + defer results.deinit(alloc); 81 + 82 + try sendJson(request, results.items); 83 + } 84 + 85 + fn handleStats(request: *http.Server.Request) !void { 86 + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 87 + defer arena.deinit(); 88 + const alloc = arena.allocator(); 89 + 90 + const stats = db.getStats(); 91 + 92 + var response: std.ArrayList(u8) = .{}; 93 + defer response.deinit(alloc); 94 + 95 + try response.print(alloc, "{{\"documents\":{d},\"publications\":{d}}}", .{ stats.documents, stats.publications }); 96 + 97 + try sendJson(request, response.items); 98 + } 99 + 100 + fn sendJson(request: *http.Server.Request, body: []const u8) !void { 101 + try request.respond(body, .{ 102 + .status = .ok, 103 + .extra_headers = &.{ 104 + .{ .name = "content-type", .value = "application/json" }, 105 + .{ .name = "access-control-allow-origin", .value = "*" }, 106 + .{ .name = "access-control-allow-methods", .value = "GET, OPTIONS" }, 107 + .{ .name = "access-control-allow-headers", .value = "content-type" }, 108 + }, 109 + }); 110 + } 111 + 112 + fn sendCorsHeaders(request: *http.Server.Request, body: []const u8) !void { 113 + try request.respond(body, .{ 114 + .status = .no_content, 115 + .extra_headers = &.{ 116 + .{ .name = "access-control-allow-origin", .value = "*" }, 117 + .{ .name = "access-control-allow-methods", .value = "GET, OPTIONS" }, 118 + .{ .name = "access-control-allow-headers", .value = "content-type" }, 119 + }, 120 + }); 121 + } 122 + 123 + fn sendNotFound(request: *http.Server.Request) !void { 124 + try request.respond("{\"error\":\"not found\"}", .{ 125 + .status = .not_found, 126 + .extra_headers = &.{ 127 + .{ .name = "content-type", .value = "application/json" }, 128 + .{ .name = "access-control-allow-origin", .value = "*" }, 129 + }, 130 + }); 131 + }
+69
backend/src/main.zig
··· 1 + const std = @import("std"); 2 + const net = std.net; 3 + const posix = std.posix; 4 + const Thread = std.Thread; 5 + const db = @import("db.zig"); 6 + const http_server = @import("http.zig"); 7 + const tap = @import("tap.zig"); 8 + 9 + const MAX_HTTP_WORKERS = 16; 10 + const SOCKET_TIMEOUT_SECS = 30; 11 + 12 + pub fn main() !void { 13 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 14 + defer _ = gpa.deinit(); 15 + const allocator = gpa.allocator(); 16 + 17 + // init turso 18 + try db.init(); 19 + defer db.close(); 20 + 21 + // start tap consumer in background 22 + const tap_thread = try Thread.spawn(.{}, tap.consumer, .{allocator}); 23 + defer tap_thread.join(); 24 + 25 + // init thread pool for http connections 26 + var pool: Thread.Pool = undefined; 27 + try pool.init(.{ 28 + .allocator = allocator, 29 + .n_jobs = MAX_HTTP_WORKERS, 30 + }); 31 + defer pool.deinit(); 32 + 33 + // start http server 34 + const port: u16 = blk: { 35 + const port_str = posix.getenv("PORT") orelse "3000"; 36 + break :blk std.fmt.parseInt(u16, port_str, 10) catch 3000; 37 + }; 38 + 39 + const address = try net.Address.parseIp("0.0.0.0", port); 40 + var server = try address.listen(.{ .reuse_address = true }); 41 + defer server.deinit(); 42 + 43 + std.debug.print("leaflet-search listening on http://0.0.0.0:{d} (max {} workers)\n", .{ port, MAX_HTTP_WORKERS }); 44 + 45 + while (true) { 46 + const conn = server.accept() catch |err| { 47 + std.debug.print("accept error: {}\n", .{err}); 48 + continue; 49 + }; 50 + 51 + setSocketTimeout(conn.stream.handle, SOCKET_TIMEOUT_SECS) catch |err| { 52 + std.debug.print("failed to set socket timeout: {}\n", .{err}); 53 + }; 54 + 55 + pool.spawn(http_server.handleConnection, .{conn}) catch |err| { 56 + std.debug.print("pool spawn error: {}\n", .{err}); 57 + conn.stream.close(); 58 + }; 59 + } 60 + } 61 + 62 + fn setSocketTimeout(fd: posix.fd_t, secs: u32) !void { 63 + const timeout = std.mem.toBytes(posix.timeval{ 64 + .sec = @intCast(secs), 65 + .usec = 0, 66 + }); 67 + try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout); 68 + try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout); 69 + }
+295
backend/src/tap.zig
··· 1 + const std = @import("std"); 2 + const mem = std.mem; 3 + const json = std.json; 4 + const posix = std.posix; 5 + const Allocator = mem.Allocator; 6 + const websocket = @import("websocket"); 7 + const db = @import("db.zig"); 8 + 9 + const DOCUMENT_COLLECTION = "pub.leaflet.document"; 10 + const PUBLICATION_COLLECTION = "pub.leaflet.publication"; 11 + 12 + fn getTapHost() []const u8 { 13 + return posix.getenv("TAP_HOST") orelse "leaflet-search-tap.fly.dev"; 14 + } 15 + 16 + fn getTapPort() u16 { 17 + const port_str = posix.getenv("TAP_PORT") orelse "443"; 18 + return std.fmt.parseInt(u16, port_str, 10) catch 443; 19 + } 20 + 21 + fn useTls() bool { 22 + return getTapPort() == 443; 23 + } 24 + 25 + pub fn consumer(allocator: Allocator) void { 26 + var backoff: u64 = 1; 27 + const max_backoff: u64 = 30; 28 + 29 + while (true) { 30 + const connected = connect(allocator); 31 + if (connected) |_| { 32 + // connection succeeded then closed - reset backoff 33 + backoff = 1; 34 + std.debug.print("tap connection closed, reconnecting immediately...\n", .{}); 35 + } else |err| { 36 + // connection failed - backoff 37 + std.debug.print("tap error: {}, reconnecting in {}s...\n", .{ err, backoff }); 38 + posix.nanosleep(backoff, 0); 39 + backoff = @min(backoff * 2, max_backoff); 40 + } 41 + } 42 + } 43 + 44 + const Handler = struct { 45 + allocator: Allocator, 46 + msg_count: usize = 0, 47 + 48 + pub fn serverMessage(self: *Handler, data: []const u8) !void { 49 + self.msg_count += 1; 50 + if (self.msg_count % 100 == 1) { 51 + std.debug.print("tap: received {} messages\n", .{self.msg_count}); 52 + } 53 + processMessage(self.allocator, data) catch |err| { 54 + std.debug.print("message processing error: {}\n", .{err}); 55 + }; 56 + } 57 + 58 + pub fn close(_: *Handler) void { 59 + std.debug.print("tap connection closed\n", .{}); 60 + } 61 + }; 62 + 63 + fn connect(allocator: Allocator) !void { 64 + const host = getTapHost(); 65 + const port = getTapPort(); 66 + const tls = useTls(); 67 + const path = "/channel"; 68 + 69 + std.debug.print("connecting to {s}://{s}:{d}{s}\n", .{ if (tls) "wss" else "ws", host, port, path }); 70 + 71 + var client = websocket.Client.init(allocator, .{ 72 + .host = host, 73 + .port = port, 74 + .tls = tls, 75 + .max_size = 1024 * 1024, // 1MB 76 + }) catch |err| { 77 + std.debug.print("websocket client init failed: {}\n", .{err}); 78 + return err; 79 + }; 80 + defer client.deinit(); 81 + 82 + var host_header_buf: [256]u8 = undefined; 83 + const host_header = std.fmt.bufPrint(&host_header_buf, "Host: {s}\r\n", .{host}) catch host; 84 + 85 + client.handshake(path, .{ .headers = host_header }) catch |err| { 86 + std.debug.print("websocket handshake failed: {}\n", .{err}); 87 + return err; 88 + }; 89 + 90 + std.debug.print("tap connected!\n", .{}); 91 + 92 + var handler = Handler{ .allocator = allocator }; 93 + client.readLoop(&handler) catch |err| { 94 + std.debug.print("websocket read loop error: {}\n", .{err}); 95 + return err; 96 + }; 97 + } 98 + 99 + fn processMessage(allocator: Allocator, payload: []const u8) !void { 100 + const parsed = json.parseFromSlice(json.Value, allocator, payload, .{}) catch return; 101 + defer parsed.deinit(); 102 + 103 + const root = parsed.value.object; 104 + 105 + // tap format: { "id": 123, "type": "record", "record": { ... } } 106 + const msg_type = root.get("type") orelse return; 107 + if (msg_type != .string) return; 108 + if (!mem.eql(u8, msg_type.string, "record")) return; 109 + 110 + const record_wrapper = root.get("record") orelse return; 111 + if (record_wrapper != .object) return; 112 + 113 + const rec = record_wrapper.object; 114 + 115 + const collection = rec.get("collection") orelse return; 116 + if (collection != .string) return; 117 + 118 + const action = rec.get("action") orelse return; 119 + if (action != .string) return; 120 + 121 + const did = rec.get("did") orelse return; 122 + if (did != .string) return; 123 + 124 + const rkey = rec.get("rkey") orelse return; 125 + if (rkey != .string) return; 126 + 127 + const uri_str = try std.fmt.allocPrint(allocator, "at://{s}/{s}/{s}", .{ did.string, collection.string, rkey.string }); 128 + defer allocator.free(uri_str); 129 + 130 + if (mem.eql(u8, action.string, "create") or mem.eql(u8, action.string, "update")) { 131 + const record = rec.get("record") orelse return; 132 + if (record != .object) return; 133 + 134 + if (mem.eql(u8, collection.string, DOCUMENT_COLLECTION)) { 135 + processDocument(allocator, uri_str, did.string, rkey.string, record.object) catch |err| { 136 + std.debug.print("document processing error: {}\n", .{err}); 137 + }; 138 + } else if (mem.eql(u8, collection.string, PUBLICATION_COLLECTION)) { 139 + processPublication(uri_str, did.string, rkey.string, record.object) catch |err| { 140 + std.debug.print("publication processing error: {}\n", .{err}); 141 + }; 142 + } 143 + } else if (mem.eql(u8, action.string, "delete")) { 144 + if (mem.eql(u8, collection.string, DOCUMENT_COLLECTION)) { 145 + db.deleteDocument(uri_str); 146 + std.debug.print("deleted document: {s}\n", .{uri_str}); 147 + } else if (mem.eql(u8, collection.string, PUBLICATION_COLLECTION)) { 148 + db.deletePublication(uri_str); 149 + std.debug.print("deleted publication: {s}\n", .{uri_str}); 150 + } 151 + } 152 + } 153 + 154 + fn processDocument(allocator: Allocator, uri: []const u8, did: []const u8, rkey: []const u8, record: json.ObjectMap) !void { 155 + // get title 156 + const title_val = record.get("title") orelse return; 157 + if (title_val != .string) return; 158 + const title = title_val.string; 159 + 160 + // get createdAt (optional, might be publishedAt) 161 + const created_at: ?[]const u8 = blk: { 162 + if (record.get("publishedAt")) |v| { 163 + if (v == .string) break :blk v.string; 164 + } 165 + if (record.get("createdAt")) |v| { 166 + if (v == .string) break :blk v.string; 167 + } 168 + break :blk null; 169 + }; 170 + 171 + var content_buf: std.ArrayList(u8) = .{}; 172 + defer content_buf.deinit(allocator); 173 + 174 + // include document description if present 175 + if (record.get("description")) |desc_val| { 176 + if (desc_val == .string and desc_val.string.len > 0) { 177 + try content_buf.appendSlice(allocator, desc_val.string); 178 + } 179 + } 180 + 181 + // extract plaintext from pages 182 + if (record.get("pages")) |pages_val| { 183 + if (pages_val == .array) { 184 + for (pages_val.array.items) |page| { 185 + if (page != .object) continue; 186 + try extractPlaintextFromPage(allocator, &content_buf, page.object); 187 + } 188 + } 189 + } 190 + 191 + if (content_buf.items.len == 0) { 192 + // no content extracted, skip 193 + return; 194 + } 195 + 196 + try db.insertDocument(uri, did, rkey, title, content_buf.items, created_at); 197 + std.debug.print("indexed document: {s} ({} chars)\n", .{ uri, content_buf.items.len }); 198 + } 199 + 200 + fn extractPlaintextFromPage(allocator: Allocator, buf: *std.ArrayList(u8), page: json.ObjectMap) !void { 201 + // pages can be linearDocument or canvas 202 + // linearDocument has blocks array 203 + const blocks_val = page.get("blocks") orelse return; 204 + if (blocks_val != .array) return; 205 + 206 + for (blocks_val.array.items) |block_wrapper| { 207 + if (block_wrapper != .object) continue; 208 + 209 + // block wrapper has "block" field with actual content 210 + const block_val = block_wrapper.object.get("block") orelse continue; 211 + if (block_val != .object) continue; 212 + 213 + try extractTextFromBlock(allocator, buf, block_val.object); 214 + } 215 + } 216 + 217 + fn extractTextFromBlock(allocator: Allocator, buf: *std.ArrayList(u8), block: json.ObjectMap) Allocator.Error!void { 218 + const type_val = block.get("$type") orelse return; 219 + if (type_val != .string) return; 220 + 221 + const block_type = type_val.string; 222 + 223 + // blocks with plaintext field: text, header, blockquote, code 224 + if (mem.eql(u8, block_type, "pub.leaflet.blocks.text") or 225 + mem.eql(u8, block_type, "pub.leaflet.blocks.header") or 226 + mem.eql(u8, block_type, "pub.leaflet.blocks.blockquote") or 227 + mem.eql(u8, block_type, "pub.leaflet.blocks.code")) 228 + { 229 + if (block.get("plaintext")) |plaintext_val| { 230 + if (plaintext_val == .string) { 231 + if (buf.items.len > 0) { 232 + try buf.appendSlice(allocator, " "); 233 + } 234 + try buf.appendSlice(allocator, plaintext_val.string); 235 + } 236 + } 237 + } 238 + // button has text field 239 + else if (mem.eql(u8, block_type, "pub.leaflet.blocks.button")) { 240 + if (block.get("text")) |text_val| { 241 + if (text_val == .string) { 242 + if (buf.items.len > 0) { 243 + try buf.appendSlice(allocator, " "); 244 + } 245 + try buf.appendSlice(allocator, text_val.string); 246 + } 247 + } 248 + } 249 + // unorderedList has children array with nested content 250 + else if (mem.eql(u8, block_type, "pub.leaflet.blocks.unorderedList")) { 251 + if (block.get("children")) |children_val| { 252 + if (children_val == .array) { 253 + for (children_val.array.items) |child| { 254 + try extractListItemText(allocator, buf, child); 255 + } 256 + } 257 + } 258 + } 259 + } 260 + 261 + fn extractListItemText(allocator: Allocator, buf: *std.ArrayList(u8), item: json.Value) Allocator.Error!void { 262 + if (item != .object) return; 263 + 264 + // list item has content field which is a block 265 + if (item.object.get("content")) |content_val| { 266 + if (content_val == .object) { 267 + try extractTextFromBlock(allocator, buf, content_val.object); 268 + } 269 + } 270 + 271 + // nested children 272 + if (item.object.get("children")) |children_val| { 273 + if (children_val == .array) { 274 + for (children_val.array.items) |child| { 275 + try extractListItemText(allocator, buf, child); 276 + } 277 + } 278 + } 279 + } 280 + 281 + fn processPublication(uri: []const u8, did: []const u8, rkey: []const u8, record: json.ObjectMap) !void { 282 + const name_val = record.get("name") orelse return; 283 + if (name_val != .string) return; 284 + const name = name_val.string; 285 + 286 + const description: ?[]const u8 = blk: { 287 + if (record.get("description")) |v| { 288 + if (v == .string) break :blk v.string; 289 + } 290 + break :blk null; 291 + }; 292 + 293 + try db.insertPublication(uri, did, rkey, name, description); 294 + std.debug.print("indexed publication: {s}\n", .{uri}); 295 + }
+29
fly.toml
··· 1 + # fly.toml - leaflet search slice 2 + app = 'leaflet-search-slice' 3 + primary_region = 'ewr' 4 + 5 + [build] 6 + image = 'ghcr.io/bigmoves/quickslice:latest' 7 + 8 + [env] 9 + DATABASE_URL = 'sqlite:/data/quickslice.db' 10 + HOST = '0.0.0.0' 11 + PORT = '8080' 12 + EXTERNAL_BASE_URL = 'https://leaflet-search-slice.fly.dev' 13 + 14 + [[mounts]] 15 + source = 'leaflet_search_data' 16 + destination = '/data' 17 + 18 + [http_service] 19 + internal_port = 8080 20 + force_https = true 21 + auto_stop_machines = 'stop' 22 + auto_start_machines = true 23 + min_machines_running = 1 24 + 25 + [[vm]] 26 + memory = '1gb' 27 + cpu_kind = 'shared' 28 + cpus = 1 29 + memory_mb = 1024
+318
site/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>leaflet search</title> 7 + <style> 8 + * { box-sizing: border-box; margin: 0; padding: 0; } 9 + 10 + body { 11 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 12 + background: #0a0a0a; 13 + color: #e5e5e5; 14 + min-height: 100vh; 15 + padding: 2rem; 16 + } 17 + 18 + .container { 19 + max-width: 800px; 20 + margin: 0 auto; 21 + } 22 + 23 + h1 { 24 + font-size: 1.5rem; 25 + font-weight: 400; 26 + margin-bottom: 1.5rem; 27 + color: #a3a3a3; 28 + } 29 + 30 + h1 span { color: #22c55e; } 31 + h1 a.src { font-size: 0.7rem; color: #525252; margin-left: 0.5rem; } 32 + h1 a.src:hover { color: #737373; } 33 + 34 + .search-box { 35 + display: flex; 36 + gap: 0.5rem; 37 + margin-bottom: 2rem; 38 + } 39 + 40 + input[type="text"] { 41 + flex: 1; 42 + padding: 0.75rem 1rem; 43 + font-size: 1rem; 44 + background: #171717; 45 + border: 1px solid #262626; 46 + border-radius: 0.5rem; 47 + color: #e5e5e5; 48 + outline: none; 49 + } 50 + 51 + input[type="text"]:focus { 52 + border-color: #22c55e; 53 + } 54 + 55 + button { 56 + padding: 0.75rem 1.5rem; 57 + font-size: 1rem; 58 + background: #22c55e; 59 + color: #0a0a0a; 60 + border: none; 61 + border-radius: 0.5rem; 62 + cursor: pointer; 63 + font-weight: 500; 64 + } 65 + 66 + button:hover { background: #16a34a; } 67 + button:disabled { background: #374151; color: #6b7280; cursor: not-allowed; } 68 + 69 + .results { 70 + display: flex; 71 + flex-direction: column; 72 + gap: 1rem; 73 + } 74 + 75 + .result { 76 + background: #171717; 77 + border: 1px solid #262626; 78 + border-radius: 0.5rem; 79 + padding: 1rem; 80 + } 81 + 82 + .result:hover { border-color: #404040; } 83 + 84 + .result-title { 85 + font-size: 1.1rem; 86 + margin-bottom: 0.5rem; 87 + color: #f5f5f5; 88 + } 89 + 90 + .result-title a { 91 + color: inherit; 92 + text-decoration: none; 93 + } 94 + 95 + .result-title a:hover { color: #22c55e; } 96 + 97 + .result-snippet { 98 + font-size: 0.9rem; 99 + color: #a3a3a3; 100 + margin-bottom: 0.5rem; 101 + line-height: 1.4; 102 + } 103 + 104 + .result-snippet mark { 105 + background: #22c55e33; 106 + color: #22c55e; 107 + padding: 0 2px; 108 + border-radius: 2px; 109 + } 110 + 111 + .result-meta { 112 + font-size: 0.75rem; 113 + color: #525252; 114 + } 115 + 116 + .status { 117 + padding: 1rem; 118 + text-align: center; 119 + color: #737373; 120 + } 121 + 122 + .error { color: #ef4444; } 123 + 124 + .empty-state { 125 + text-align: center; 126 + padding: 3rem; 127 + color: #525252; 128 + } 129 + 130 + .empty-state p { margin-bottom: 0.5rem; } 131 + 132 + .stats { 133 + font-size: 0.75rem; 134 + color: #525252; 135 + margin-top: 2rem; 136 + text-align: center; 137 + } 138 + </style> 139 + </head> 140 + <body> 141 + <div class="container"> 142 + <h1><span>leaflet</span> search <a href="https://tangled.sh/@zzstoatzz.io/leaflet-search" target="_blank" class="src">[src]</a></h1> 143 + 144 + <div class="search-box"> 145 + <input type="text" id="query" placeholder="search content..." autofocus> 146 + <button id="search-btn">search</button> 147 + </div> 148 + 149 + <div id="results" class="results"> 150 + <div class="empty-state"> 151 + <p>full-text search for leaflet documents</p> 152 + <p>searches titles and content</p> 153 + </div> 154 + </div> 155 + 156 + <div id="stats" class="stats"></div> 157 + </div> 158 + 159 + <script> 160 + const API_URL = 'https://leaflet-search-backend.fly.dev'; 161 + 162 + const queryInput = document.getElementById('query'); 163 + const searchBtn = document.getElementById('search-btn'); 164 + const resultsDiv = document.getElementById('results'); 165 + const statsDiv = document.getElementById('stats'); 166 + 167 + async function search(query) { 168 + if (!query.trim()) return; 169 + 170 + searchBtn.disabled = true; 171 + const searchUrl = `${API_URL}/search?q=${encodeURIComponent(query)}`; 172 + resultsDiv.innerHTML = `<div class="status">searching...<br><code style="font-size:0.7rem;color:#525252">${searchUrl}</code></div>`; 173 + 174 + try { 175 + const res = await fetch(searchUrl); 176 + const rawText = await res.text(); 177 + let results; 178 + 179 + try { 180 + results = JSON.parse(rawText); 181 + } catch (parseErr) { 182 + resultsDiv.innerHTML = `<div class="status error">JSON parse error<pre style="text-align:left;font-size:0.7rem;overflow:auto;max-height:200px">${escapeHtml(rawText)}</pre></div>`; 183 + return; 184 + } 185 + 186 + if (results.error) { 187 + resultsDiv.innerHTML = `<div class="status error">${results.error}</div>`; 188 + return; 189 + } 190 + 191 + if (results.length === 0) { 192 + resultsDiv.innerHTML = ` 193 + <div class="empty-state"> 194 + <p>no results for "${escapeHtml(query)}"</p> 195 + <p>try different keywords</p> 196 + <p style="margin-top:1rem;font-size:0.7rem;color:#404040">query sent: <code>${searchUrl}</code></p> 197 + <p style="font-size:0.7rem;color:#404040">response: <code>${escapeHtml(rawText)}</code></p> 198 + </div> 199 + `; 200 + statsDiv.textContent = ''; 201 + return; 202 + } 203 + 204 + let html = `<div style="font-size:0.7rem;color:#404040;margin-bottom:1rem;padding:0.5rem;background:#111;border-radius:4px"> 205 + query: <code>${escapeHtml(query)}</code> | url: <code>${searchUrl}</code> | ${results.length} results 206 + </div>`; 207 + 208 + // resolve all unique DIDs in parallel 209 + const uniqueDids = [...new Set(results.map(d => parseUri(d.uri).did))]; 210 + await Promise.all(uniqueDids.map(did => resolveHandle(did))); 211 + 212 + for (const doc of results) { 213 + const { did, rkey } = parseUri(doc.uri); 214 + const handle = handleCache.get(did); 215 + const leafletUrl = handle ? buildLeafletUrl(handle, rkey) : `https://leaflet.pub/${rkey}`; 216 + const date = doc.createdAt ? new Date(doc.createdAt).toLocaleDateString() : ''; 217 + html += ` 218 + <div class="result"> 219 + <div class="result-title"> 220 + <a href="${leafletUrl}" target="_blank">${escapeHtml(doc.title || 'Untitled')}</a> 221 + </div> 222 + <div class="result-snippet">${doc.snippet || ''}</div> 223 + <div class="result-meta"> 224 + ${date ? `${date} | ` : ''}${handle ? `@${handle} | ` : ''}uri: ${escapeHtml(doc.uri)} 225 + </div> 226 + </div> 227 + `; 228 + } 229 + 230 + resultsDiv.innerHTML = html; 231 + statsDiv.textContent = `${results.length} results`; 232 + 233 + } catch (err) { 234 + resultsDiv.innerHTML = `<div class="status error">error: ${err.message}<br><code style="font-size:0.7rem">${searchUrl}</code></div>`; 235 + } finally { 236 + searchBtn.disabled = false; 237 + } 238 + } 239 + 240 + // cache DID -> handle resolutions 241 + const handleCache = new Map(); 242 + 243 + async function resolveHandle(did) { 244 + if (handleCache.has(did)) return handleCache.get(did); 245 + try { 246 + const res = await fetch(`https://plc.directory/${did}`); 247 + const doc = await res.json(); 248 + // alsoKnownAs contains at://handle entries 249 + const aka = doc.alsoKnownAs?.find(u => u.startsWith('at://')); 250 + const handle = aka ? aka.replace('at://', '') : null; 251 + handleCache.set(did, handle); 252 + return handle; 253 + } catch { 254 + handleCache.set(did, null); 255 + return null; 256 + } 257 + } 258 + 259 + function parseUri(uri) { 260 + // at://did:plc:xxx/pub.leaflet.document/rkey 261 + const parts = uri.replace('at://', '').split('/'); 262 + return { did: parts[0], collection: parts[1], rkey: parts[2] }; 263 + } 264 + 265 + function buildLeafletUrl(handle, rkey) { 266 + // handle like "nate.io" -> nate (subdomain is first part before .) 267 + const subdomain = handle.split('.')[0]; 268 + return `https://${subdomain}.leaflet.pub/${rkey}`; 269 + } 270 + 271 + function escapeHtml(str) { 272 + if (!str) return ''; 273 + return str.replace(/[&<>"']/g, c => ({ 274 + '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' 275 + })[c]); 276 + } 277 + 278 + searchBtn.addEventListener('click', () => { 279 + const q = queryInput.value; 280 + if (q.trim()) history.pushState(null, '', `?q=${encodeURIComponent(q)}`); 281 + search(q); 282 + }); 283 + queryInput.addEventListener('keydown', e => { 284 + if (e.key === 'Enter') { 285 + const q = queryInput.value; 286 + if (q.trim()) history.pushState(null, '', `?q=${encodeURIComponent(q)}`); 287 + search(q); 288 + } 289 + }); 290 + 291 + // handle back/forward navigation 292 + window.addEventListener('popstate', () => { 293 + const params = new URLSearchParams(location.search); 294 + const q = params.get('q') || ''; 295 + queryInput.value = q; 296 + if (q) search(q); 297 + }); 298 + 299 + // check for ?q= on page load 300 + const initialParams = new URLSearchParams(location.search); 301 + const initialQuery = initialParams.get('q'); 302 + if (initialQuery) { 303 + queryInput.value = initialQuery; 304 + search(initialQuery); 305 + } 306 + 307 + // check backend health on load 308 + fetch(`${API_URL}/stats`) 309 + .then(r => r.json()) 310 + .then(data => { 311 + statsDiv.textContent = `index: ${data.documents} documents, ${data.publications} publications`; 312 + }) 313 + .catch(() => { 314 + statsDiv.textContent = 'backend: connecting...'; 315 + }); 316 + </script> 317 + </body> 318 + </html>
+1535
site/package-lock.json
··· 1 + { 2 + "name": "leaflet-search-site", 3 + "lockfileVersion": 3, 4 + "requires": true, 5 + "packages": { 6 + "": { 7 + "name": "leaflet-search-site", 8 + "devDependencies": { 9 + "wrangler": "^4.0.0" 10 + } 11 + }, 12 + "node_modules/@cloudflare/kv-asset-handler": { 13 + "version": "0.4.1", 14 + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.1.tgz", 15 + "integrity": "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==", 16 + "dev": true, 17 + "license": "MIT OR Apache-2.0", 18 + "dependencies": { 19 + "mime": "^3.0.0" 20 + }, 21 + "engines": { 22 + "node": ">=18.0.0" 23 + } 24 + }, 25 + "node_modules/@cloudflare/unenv-preset": { 26 + "version": "2.7.13", 27 + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.13.tgz", 28 + "integrity": "sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==", 29 + "dev": true, 30 + "license": "MIT OR Apache-2.0", 31 + "peerDependencies": { 32 + "unenv": "2.0.0-rc.24", 33 + "workerd": "^1.20251202.0" 34 + }, 35 + "peerDependenciesMeta": { 36 + "workerd": { 37 + "optional": true 38 + } 39 + } 40 + }, 41 + "node_modules/@cloudflare/workerd-darwin-64": { 42 + "version": "1.20251210.0", 43 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251210.0.tgz", 44 + "integrity": "sha512-Nn9X1moUDERA9xtFdCQ2XpQXgAS9pOjiCxvOT8sVx9UJLAiBLkfSCGbpsYdarODGybXCpjRlc77Yppuolvt7oQ==", 45 + "cpu": [ 46 + "x64" 47 + ], 48 + "dev": true, 49 + "license": "Apache-2.0", 50 + "optional": true, 51 + "os": [ 52 + "darwin" 53 + ], 54 + "engines": { 55 + "node": ">=16" 56 + } 57 + }, 58 + "node_modules/@cloudflare/workerd-darwin-arm64": { 59 + "version": "1.20251210.0", 60 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251210.0.tgz", 61 + "integrity": "sha512-Mg8iYIZQFnbevq/ls9eW/eneWTk/EE13Pej1MwfkY5et0jVpdHnvOLywy/o+QtMJFef1AjsqXGULwAneYyBfHw==", 62 + "cpu": [ 63 + "arm64" 64 + ], 65 + "dev": true, 66 + "license": "Apache-2.0", 67 + "optional": true, 68 + "os": [ 69 + "darwin" 70 + ], 71 + "engines": { 72 + "node": ">=16" 73 + } 74 + }, 75 + "node_modules/@cloudflare/workerd-linux-64": { 76 + "version": "1.20251210.0", 77 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251210.0.tgz", 78 + "integrity": "sha512-kjC2fCZhZ2Gkm1biwk2qByAYpGguK5Gf5ic8owzSCUw0FOUfQxTZUT9Lp3gApxsfTLbbnLBrX/xzWjywH9QR4g==", 79 + "cpu": [ 80 + "x64" 81 + ], 82 + "dev": true, 83 + "license": "Apache-2.0", 84 + "optional": true, 85 + "os": [ 86 + "linux" 87 + ], 88 + "engines": { 89 + "node": ">=16" 90 + } 91 + }, 92 + "node_modules/@cloudflare/workerd-linux-arm64": { 93 + "version": "1.20251210.0", 94 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251210.0.tgz", 95 + "integrity": "sha512-2IB37nXi7PZVQLa1OCuO7/6pNxqisRSO8DmCQ5x/3sezI5op1vwOxAcb1osAnuVsVN9bbvpw70HJvhKruFJTuA==", 96 + "cpu": [ 97 + "arm64" 98 + ], 99 + "dev": true, 100 + "license": "Apache-2.0", 101 + "optional": true, 102 + "os": [ 103 + "linux" 104 + ], 105 + "engines": { 106 + "node": ">=16" 107 + } 108 + }, 109 + "node_modules/@cloudflare/workerd-windows-64": { 110 + "version": "1.20251210.0", 111 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251210.0.tgz", 112 + "integrity": "sha512-Uaz6/9XE+D6E7pCY4OvkCuJHu7HcSDzeGcCGY1HLhojXhHd7yL52c3yfiyJdS8hPatiAa0nn5qSI/42+aTdDSw==", 113 + "cpu": [ 114 + "x64" 115 + ], 116 + "dev": true, 117 + "license": "Apache-2.0", 118 + "optional": true, 119 + "os": [ 120 + "win32" 121 + ], 122 + "engines": { 123 + "node": ">=16" 124 + } 125 + }, 126 + "node_modules/@cspotcode/source-map-support": { 127 + "version": "0.8.1", 128 + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 129 + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 130 + "dev": true, 131 + "license": "MIT", 132 + "dependencies": { 133 + "@jridgewell/trace-mapping": "0.3.9" 134 + }, 135 + "engines": { 136 + "node": ">=12" 137 + } 138 + }, 139 + "node_modules/@emnapi/runtime": { 140 + "version": "1.7.1", 141 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", 142 + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", 143 + "dev": true, 144 + "license": "MIT", 145 + "optional": true, 146 + "dependencies": { 147 + "tslib": "^2.4.0" 148 + } 149 + }, 150 + "node_modules/@esbuild/aix-ppc64": { 151 + "version": "0.27.0", 152 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", 153 + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", 154 + "cpu": [ 155 + "ppc64" 156 + ], 157 + "dev": true, 158 + "license": "MIT", 159 + "optional": true, 160 + "os": [ 161 + "aix" 162 + ], 163 + "engines": { 164 + "node": ">=18" 165 + } 166 + }, 167 + "node_modules/@esbuild/android-arm": { 168 + "version": "0.27.0", 169 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", 170 + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", 171 + "cpu": [ 172 + "arm" 173 + ], 174 + "dev": true, 175 + "license": "MIT", 176 + "optional": true, 177 + "os": [ 178 + "android" 179 + ], 180 + "engines": { 181 + "node": ">=18" 182 + } 183 + }, 184 + "node_modules/@esbuild/android-arm64": { 185 + "version": "0.27.0", 186 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", 187 + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", 188 + "cpu": [ 189 + "arm64" 190 + ], 191 + "dev": true, 192 + "license": "MIT", 193 + "optional": true, 194 + "os": [ 195 + "android" 196 + ], 197 + "engines": { 198 + "node": ">=18" 199 + } 200 + }, 201 + "node_modules/@esbuild/android-x64": { 202 + "version": "0.27.0", 203 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", 204 + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", 205 + "cpu": [ 206 + "x64" 207 + ], 208 + "dev": true, 209 + "license": "MIT", 210 + "optional": true, 211 + "os": [ 212 + "android" 213 + ], 214 + "engines": { 215 + "node": ">=18" 216 + } 217 + }, 218 + "node_modules/@esbuild/darwin-arm64": { 219 + "version": "0.27.0", 220 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", 221 + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", 222 + "cpu": [ 223 + "arm64" 224 + ], 225 + "dev": true, 226 + "license": "MIT", 227 + "optional": true, 228 + "os": [ 229 + "darwin" 230 + ], 231 + "engines": { 232 + "node": ">=18" 233 + } 234 + }, 235 + "node_modules/@esbuild/darwin-x64": { 236 + "version": "0.27.0", 237 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", 238 + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", 239 + "cpu": [ 240 + "x64" 241 + ], 242 + "dev": true, 243 + "license": "MIT", 244 + "optional": true, 245 + "os": [ 246 + "darwin" 247 + ], 248 + "engines": { 249 + "node": ">=18" 250 + } 251 + }, 252 + "node_modules/@esbuild/freebsd-arm64": { 253 + "version": "0.27.0", 254 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", 255 + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", 256 + "cpu": [ 257 + "arm64" 258 + ], 259 + "dev": true, 260 + "license": "MIT", 261 + "optional": true, 262 + "os": [ 263 + "freebsd" 264 + ], 265 + "engines": { 266 + "node": ">=18" 267 + } 268 + }, 269 + "node_modules/@esbuild/freebsd-x64": { 270 + "version": "0.27.0", 271 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", 272 + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", 273 + "cpu": [ 274 + "x64" 275 + ], 276 + "dev": true, 277 + "license": "MIT", 278 + "optional": true, 279 + "os": [ 280 + "freebsd" 281 + ], 282 + "engines": { 283 + "node": ">=18" 284 + } 285 + }, 286 + "node_modules/@esbuild/linux-arm": { 287 + "version": "0.27.0", 288 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", 289 + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", 290 + "cpu": [ 291 + "arm" 292 + ], 293 + "dev": true, 294 + "license": "MIT", 295 + "optional": true, 296 + "os": [ 297 + "linux" 298 + ], 299 + "engines": { 300 + "node": ">=18" 301 + } 302 + }, 303 + "node_modules/@esbuild/linux-arm64": { 304 + "version": "0.27.0", 305 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", 306 + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", 307 + "cpu": [ 308 + "arm64" 309 + ], 310 + "dev": true, 311 + "license": "MIT", 312 + "optional": true, 313 + "os": [ 314 + "linux" 315 + ], 316 + "engines": { 317 + "node": ">=18" 318 + } 319 + }, 320 + "node_modules/@esbuild/linux-ia32": { 321 + "version": "0.27.0", 322 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", 323 + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", 324 + "cpu": [ 325 + "ia32" 326 + ], 327 + "dev": true, 328 + "license": "MIT", 329 + "optional": true, 330 + "os": [ 331 + "linux" 332 + ], 333 + "engines": { 334 + "node": ">=18" 335 + } 336 + }, 337 + "node_modules/@esbuild/linux-loong64": { 338 + "version": "0.27.0", 339 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", 340 + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", 341 + "cpu": [ 342 + "loong64" 343 + ], 344 + "dev": true, 345 + "license": "MIT", 346 + "optional": true, 347 + "os": [ 348 + "linux" 349 + ], 350 + "engines": { 351 + "node": ">=18" 352 + } 353 + }, 354 + "node_modules/@esbuild/linux-mips64el": { 355 + "version": "0.27.0", 356 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", 357 + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", 358 + "cpu": [ 359 + "mips64el" 360 + ], 361 + "dev": true, 362 + "license": "MIT", 363 + "optional": true, 364 + "os": [ 365 + "linux" 366 + ], 367 + "engines": { 368 + "node": ">=18" 369 + } 370 + }, 371 + "node_modules/@esbuild/linux-ppc64": { 372 + "version": "0.27.0", 373 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", 374 + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", 375 + "cpu": [ 376 + "ppc64" 377 + ], 378 + "dev": true, 379 + "license": "MIT", 380 + "optional": true, 381 + "os": [ 382 + "linux" 383 + ], 384 + "engines": { 385 + "node": ">=18" 386 + } 387 + }, 388 + "node_modules/@esbuild/linux-riscv64": { 389 + "version": "0.27.0", 390 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", 391 + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", 392 + "cpu": [ 393 + "riscv64" 394 + ], 395 + "dev": true, 396 + "license": "MIT", 397 + "optional": true, 398 + "os": [ 399 + "linux" 400 + ], 401 + "engines": { 402 + "node": ">=18" 403 + } 404 + }, 405 + "node_modules/@esbuild/linux-s390x": { 406 + "version": "0.27.0", 407 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", 408 + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", 409 + "cpu": [ 410 + "s390x" 411 + ], 412 + "dev": true, 413 + "license": "MIT", 414 + "optional": true, 415 + "os": [ 416 + "linux" 417 + ], 418 + "engines": { 419 + "node": ">=18" 420 + } 421 + }, 422 + "node_modules/@esbuild/linux-x64": { 423 + "version": "0.27.0", 424 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", 425 + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", 426 + "cpu": [ 427 + "x64" 428 + ], 429 + "dev": true, 430 + "license": "MIT", 431 + "optional": true, 432 + "os": [ 433 + "linux" 434 + ], 435 + "engines": { 436 + "node": ">=18" 437 + } 438 + }, 439 + "node_modules/@esbuild/netbsd-arm64": { 440 + "version": "0.27.0", 441 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", 442 + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", 443 + "cpu": [ 444 + "arm64" 445 + ], 446 + "dev": true, 447 + "license": "MIT", 448 + "optional": true, 449 + "os": [ 450 + "netbsd" 451 + ], 452 + "engines": { 453 + "node": ">=18" 454 + } 455 + }, 456 + "node_modules/@esbuild/netbsd-x64": { 457 + "version": "0.27.0", 458 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", 459 + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", 460 + "cpu": [ 461 + "x64" 462 + ], 463 + "dev": true, 464 + "license": "MIT", 465 + "optional": true, 466 + "os": [ 467 + "netbsd" 468 + ], 469 + "engines": { 470 + "node": ">=18" 471 + } 472 + }, 473 + "node_modules/@esbuild/openbsd-arm64": { 474 + "version": "0.27.0", 475 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", 476 + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", 477 + "cpu": [ 478 + "arm64" 479 + ], 480 + "dev": true, 481 + "license": "MIT", 482 + "optional": true, 483 + "os": [ 484 + "openbsd" 485 + ], 486 + "engines": { 487 + "node": ">=18" 488 + } 489 + }, 490 + "node_modules/@esbuild/openbsd-x64": { 491 + "version": "0.27.0", 492 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", 493 + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", 494 + "cpu": [ 495 + "x64" 496 + ], 497 + "dev": true, 498 + "license": "MIT", 499 + "optional": true, 500 + "os": [ 501 + "openbsd" 502 + ], 503 + "engines": { 504 + "node": ">=18" 505 + } 506 + }, 507 + "node_modules/@esbuild/openharmony-arm64": { 508 + "version": "0.27.0", 509 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", 510 + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", 511 + "cpu": [ 512 + "arm64" 513 + ], 514 + "dev": true, 515 + "license": "MIT", 516 + "optional": true, 517 + "os": [ 518 + "openharmony" 519 + ], 520 + "engines": { 521 + "node": ">=18" 522 + } 523 + }, 524 + "node_modules/@esbuild/sunos-x64": { 525 + "version": "0.27.0", 526 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", 527 + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", 528 + "cpu": [ 529 + "x64" 530 + ], 531 + "dev": true, 532 + "license": "MIT", 533 + "optional": true, 534 + "os": [ 535 + "sunos" 536 + ], 537 + "engines": { 538 + "node": ">=18" 539 + } 540 + }, 541 + "node_modules/@esbuild/win32-arm64": { 542 + "version": "0.27.0", 543 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", 544 + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", 545 + "cpu": [ 546 + "arm64" 547 + ], 548 + "dev": true, 549 + "license": "MIT", 550 + "optional": true, 551 + "os": [ 552 + "win32" 553 + ], 554 + "engines": { 555 + "node": ">=18" 556 + } 557 + }, 558 + "node_modules/@esbuild/win32-ia32": { 559 + "version": "0.27.0", 560 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", 561 + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", 562 + "cpu": [ 563 + "ia32" 564 + ], 565 + "dev": true, 566 + "license": "MIT", 567 + "optional": true, 568 + "os": [ 569 + "win32" 570 + ], 571 + "engines": { 572 + "node": ">=18" 573 + } 574 + }, 575 + "node_modules/@esbuild/win32-x64": { 576 + "version": "0.27.0", 577 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", 578 + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", 579 + "cpu": [ 580 + "x64" 581 + ], 582 + "dev": true, 583 + "license": "MIT", 584 + "optional": true, 585 + "os": [ 586 + "win32" 587 + ], 588 + "engines": { 589 + "node": ">=18" 590 + } 591 + }, 592 + "node_modules/@img/sharp-darwin-arm64": { 593 + "version": "0.33.5", 594 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", 595 + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", 596 + "cpu": [ 597 + "arm64" 598 + ], 599 + "dev": true, 600 + "license": "Apache-2.0", 601 + "optional": true, 602 + "os": [ 603 + "darwin" 604 + ], 605 + "engines": { 606 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 607 + }, 608 + "funding": { 609 + "url": "https://opencollective.com/libvips" 610 + }, 611 + "optionalDependencies": { 612 + "@img/sharp-libvips-darwin-arm64": "1.0.4" 613 + } 614 + }, 615 + "node_modules/@img/sharp-darwin-x64": { 616 + "version": "0.33.5", 617 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", 618 + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", 619 + "cpu": [ 620 + "x64" 621 + ], 622 + "dev": true, 623 + "license": "Apache-2.0", 624 + "optional": true, 625 + "os": [ 626 + "darwin" 627 + ], 628 + "engines": { 629 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 630 + }, 631 + "funding": { 632 + "url": "https://opencollective.com/libvips" 633 + }, 634 + "optionalDependencies": { 635 + "@img/sharp-libvips-darwin-x64": "1.0.4" 636 + } 637 + }, 638 + "node_modules/@img/sharp-libvips-darwin-arm64": { 639 + "version": "1.0.4", 640 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", 641 + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", 642 + "cpu": [ 643 + "arm64" 644 + ], 645 + "dev": true, 646 + "license": "LGPL-3.0-or-later", 647 + "optional": true, 648 + "os": [ 649 + "darwin" 650 + ], 651 + "funding": { 652 + "url": "https://opencollective.com/libvips" 653 + } 654 + }, 655 + "node_modules/@img/sharp-libvips-darwin-x64": { 656 + "version": "1.0.4", 657 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", 658 + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", 659 + "cpu": [ 660 + "x64" 661 + ], 662 + "dev": true, 663 + "license": "LGPL-3.0-or-later", 664 + "optional": true, 665 + "os": [ 666 + "darwin" 667 + ], 668 + "funding": { 669 + "url": "https://opencollective.com/libvips" 670 + } 671 + }, 672 + "node_modules/@img/sharp-libvips-linux-arm": { 673 + "version": "1.0.5", 674 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", 675 + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", 676 + "cpu": [ 677 + "arm" 678 + ], 679 + "dev": true, 680 + "license": "LGPL-3.0-or-later", 681 + "optional": true, 682 + "os": [ 683 + "linux" 684 + ], 685 + "funding": { 686 + "url": "https://opencollective.com/libvips" 687 + } 688 + }, 689 + "node_modules/@img/sharp-libvips-linux-arm64": { 690 + "version": "1.0.4", 691 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", 692 + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", 693 + "cpu": [ 694 + "arm64" 695 + ], 696 + "dev": true, 697 + "license": "LGPL-3.0-or-later", 698 + "optional": true, 699 + "os": [ 700 + "linux" 701 + ], 702 + "funding": { 703 + "url": "https://opencollective.com/libvips" 704 + } 705 + }, 706 + "node_modules/@img/sharp-libvips-linux-s390x": { 707 + "version": "1.0.4", 708 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", 709 + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", 710 + "cpu": [ 711 + "s390x" 712 + ], 713 + "dev": true, 714 + "license": "LGPL-3.0-or-later", 715 + "optional": true, 716 + "os": [ 717 + "linux" 718 + ], 719 + "funding": { 720 + "url": "https://opencollective.com/libvips" 721 + } 722 + }, 723 + "node_modules/@img/sharp-libvips-linux-x64": { 724 + "version": "1.0.4", 725 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", 726 + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", 727 + "cpu": [ 728 + "x64" 729 + ], 730 + "dev": true, 731 + "license": "LGPL-3.0-or-later", 732 + "optional": true, 733 + "os": [ 734 + "linux" 735 + ], 736 + "funding": { 737 + "url": "https://opencollective.com/libvips" 738 + } 739 + }, 740 + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { 741 + "version": "1.0.4", 742 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", 743 + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", 744 + "cpu": [ 745 + "arm64" 746 + ], 747 + "dev": true, 748 + "license": "LGPL-3.0-or-later", 749 + "optional": true, 750 + "os": [ 751 + "linux" 752 + ], 753 + "funding": { 754 + "url": "https://opencollective.com/libvips" 755 + } 756 + }, 757 + "node_modules/@img/sharp-libvips-linuxmusl-x64": { 758 + "version": "1.0.4", 759 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", 760 + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", 761 + "cpu": [ 762 + "x64" 763 + ], 764 + "dev": true, 765 + "license": "LGPL-3.0-or-later", 766 + "optional": true, 767 + "os": [ 768 + "linux" 769 + ], 770 + "funding": { 771 + "url": "https://opencollective.com/libvips" 772 + } 773 + }, 774 + "node_modules/@img/sharp-linux-arm": { 775 + "version": "0.33.5", 776 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", 777 + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", 778 + "cpu": [ 779 + "arm" 780 + ], 781 + "dev": true, 782 + "license": "Apache-2.0", 783 + "optional": true, 784 + "os": [ 785 + "linux" 786 + ], 787 + "engines": { 788 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 789 + }, 790 + "funding": { 791 + "url": "https://opencollective.com/libvips" 792 + }, 793 + "optionalDependencies": { 794 + "@img/sharp-libvips-linux-arm": "1.0.5" 795 + } 796 + }, 797 + "node_modules/@img/sharp-linux-arm64": { 798 + "version": "0.33.5", 799 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", 800 + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", 801 + "cpu": [ 802 + "arm64" 803 + ], 804 + "dev": true, 805 + "license": "Apache-2.0", 806 + "optional": true, 807 + "os": [ 808 + "linux" 809 + ], 810 + "engines": { 811 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 812 + }, 813 + "funding": { 814 + "url": "https://opencollective.com/libvips" 815 + }, 816 + "optionalDependencies": { 817 + "@img/sharp-libvips-linux-arm64": "1.0.4" 818 + } 819 + }, 820 + "node_modules/@img/sharp-linux-s390x": { 821 + "version": "0.33.5", 822 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", 823 + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", 824 + "cpu": [ 825 + "s390x" 826 + ], 827 + "dev": true, 828 + "license": "Apache-2.0", 829 + "optional": true, 830 + "os": [ 831 + "linux" 832 + ], 833 + "engines": { 834 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 835 + }, 836 + "funding": { 837 + "url": "https://opencollective.com/libvips" 838 + }, 839 + "optionalDependencies": { 840 + "@img/sharp-libvips-linux-s390x": "1.0.4" 841 + } 842 + }, 843 + "node_modules/@img/sharp-linux-x64": { 844 + "version": "0.33.5", 845 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", 846 + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", 847 + "cpu": [ 848 + "x64" 849 + ], 850 + "dev": true, 851 + "license": "Apache-2.0", 852 + "optional": true, 853 + "os": [ 854 + "linux" 855 + ], 856 + "engines": { 857 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 858 + }, 859 + "funding": { 860 + "url": "https://opencollective.com/libvips" 861 + }, 862 + "optionalDependencies": { 863 + "@img/sharp-libvips-linux-x64": "1.0.4" 864 + } 865 + }, 866 + "node_modules/@img/sharp-linuxmusl-arm64": { 867 + "version": "0.33.5", 868 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", 869 + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", 870 + "cpu": [ 871 + "arm64" 872 + ], 873 + "dev": true, 874 + "license": "Apache-2.0", 875 + "optional": true, 876 + "os": [ 877 + "linux" 878 + ], 879 + "engines": { 880 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 881 + }, 882 + "funding": { 883 + "url": "https://opencollective.com/libvips" 884 + }, 885 + "optionalDependencies": { 886 + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" 887 + } 888 + }, 889 + "node_modules/@img/sharp-linuxmusl-x64": { 890 + "version": "0.33.5", 891 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", 892 + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", 893 + "cpu": [ 894 + "x64" 895 + ], 896 + "dev": true, 897 + "license": "Apache-2.0", 898 + "optional": true, 899 + "os": [ 900 + "linux" 901 + ], 902 + "engines": { 903 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 904 + }, 905 + "funding": { 906 + "url": "https://opencollective.com/libvips" 907 + }, 908 + "optionalDependencies": { 909 + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" 910 + } 911 + }, 912 + "node_modules/@img/sharp-wasm32": { 913 + "version": "0.33.5", 914 + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", 915 + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", 916 + "cpu": [ 917 + "wasm32" 918 + ], 919 + "dev": true, 920 + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", 921 + "optional": true, 922 + "dependencies": { 923 + "@emnapi/runtime": "^1.2.0" 924 + }, 925 + "engines": { 926 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 927 + }, 928 + "funding": { 929 + "url": "https://opencollective.com/libvips" 930 + } 931 + }, 932 + "node_modules/@img/sharp-win32-ia32": { 933 + "version": "0.33.5", 934 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", 935 + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", 936 + "cpu": [ 937 + "ia32" 938 + ], 939 + "dev": true, 940 + "license": "Apache-2.0 AND LGPL-3.0-or-later", 941 + "optional": true, 942 + "os": [ 943 + "win32" 944 + ], 945 + "engines": { 946 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 947 + }, 948 + "funding": { 949 + "url": "https://opencollective.com/libvips" 950 + } 951 + }, 952 + "node_modules/@img/sharp-win32-x64": { 953 + "version": "0.33.5", 954 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", 955 + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", 956 + "cpu": [ 957 + "x64" 958 + ], 959 + "dev": true, 960 + "license": "Apache-2.0 AND LGPL-3.0-or-later", 961 + "optional": true, 962 + "os": [ 963 + "win32" 964 + ], 965 + "engines": { 966 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 967 + }, 968 + "funding": { 969 + "url": "https://opencollective.com/libvips" 970 + } 971 + }, 972 + "node_modules/@jridgewell/resolve-uri": { 973 + "version": "3.1.2", 974 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 975 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 976 + "dev": true, 977 + "license": "MIT", 978 + "engines": { 979 + "node": ">=6.0.0" 980 + } 981 + }, 982 + "node_modules/@jridgewell/sourcemap-codec": { 983 + "version": "1.5.5", 984 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 985 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 986 + "dev": true, 987 + "license": "MIT" 988 + }, 989 + "node_modules/@jridgewell/trace-mapping": { 990 + "version": "0.3.9", 991 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 992 + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 993 + "dev": true, 994 + "license": "MIT", 995 + "dependencies": { 996 + "@jridgewell/resolve-uri": "^3.0.3", 997 + "@jridgewell/sourcemap-codec": "^1.4.10" 998 + } 999 + }, 1000 + "node_modules/@poppinss/colors": { 1001 + "version": "4.1.6", 1002 + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", 1003 + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", 1004 + "dev": true, 1005 + "license": "MIT", 1006 + "dependencies": { 1007 + "kleur": "^4.1.5" 1008 + } 1009 + }, 1010 + "node_modules/@poppinss/dumper": { 1011 + "version": "0.6.5", 1012 + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", 1013 + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", 1014 + "dev": true, 1015 + "license": "MIT", 1016 + "dependencies": { 1017 + "@poppinss/colors": "^4.1.5", 1018 + "@sindresorhus/is": "^7.0.2", 1019 + "supports-color": "^10.0.0" 1020 + } 1021 + }, 1022 + "node_modules/@poppinss/exception": { 1023 + "version": "1.2.3", 1024 + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", 1025 + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", 1026 + "dev": true, 1027 + "license": "MIT" 1028 + }, 1029 + "node_modules/@sindresorhus/is": { 1030 + "version": "7.2.0", 1031 + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", 1032 + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", 1033 + "dev": true, 1034 + "license": "MIT", 1035 + "engines": { 1036 + "node": ">=18" 1037 + }, 1038 + "funding": { 1039 + "url": "https://github.com/sindresorhus/is?sponsor=1" 1040 + } 1041 + }, 1042 + "node_modules/@speed-highlight/core": { 1043 + "version": "1.2.14", 1044 + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz", 1045 + "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", 1046 + "dev": true, 1047 + "license": "CC0-1.0" 1048 + }, 1049 + "node_modules/acorn": { 1050 + "version": "8.14.0", 1051 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 1052 + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 1053 + "dev": true, 1054 + "license": "MIT", 1055 + "bin": { 1056 + "acorn": "bin/acorn" 1057 + }, 1058 + "engines": { 1059 + "node": ">=0.4.0" 1060 + } 1061 + }, 1062 + "node_modules/acorn-walk": { 1063 + "version": "8.3.2", 1064 + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", 1065 + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", 1066 + "dev": true, 1067 + "license": "MIT", 1068 + "engines": { 1069 + "node": ">=0.4.0" 1070 + } 1071 + }, 1072 + "node_modules/blake3-wasm": { 1073 + "version": "2.1.5", 1074 + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", 1075 + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", 1076 + "dev": true, 1077 + "license": "MIT" 1078 + }, 1079 + "node_modules/color": { 1080 + "version": "4.2.3", 1081 + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 1082 + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 1083 + "dev": true, 1084 + "license": "MIT", 1085 + "dependencies": { 1086 + "color-convert": "^2.0.1", 1087 + "color-string": "^1.9.0" 1088 + }, 1089 + "engines": { 1090 + "node": ">=12.5.0" 1091 + } 1092 + }, 1093 + "node_modules/color-convert": { 1094 + "version": "2.0.1", 1095 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1096 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1097 + "dev": true, 1098 + "license": "MIT", 1099 + "dependencies": { 1100 + "color-name": "~1.1.4" 1101 + }, 1102 + "engines": { 1103 + "node": ">=7.0.0" 1104 + } 1105 + }, 1106 + "node_modules/color-name": { 1107 + "version": "1.1.4", 1108 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1109 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1110 + "dev": true, 1111 + "license": "MIT" 1112 + }, 1113 + "node_modules/color-string": { 1114 + "version": "1.9.1", 1115 + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 1116 + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 1117 + "dev": true, 1118 + "license": "MIT", 1119 + "dependencies": { 1120 + "color-name": "^1.0.0", 1121 + "simple-swizzle": "^0.2.2" 1122 + } 1123 + }, 1124 + "node_modules/cookie": { 1125 + "version": "1.1.1", 1126 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", 1127 + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", 1128 + "dev": true, 1129 + "license": "MIT", 1130 + "engines": { 1131 + "node": ">=18" 1132 + }, 1133 + "funding": { 1134 + "type": "opencollective", 1135 + "url": "https://opencollective.com/express" 1136 + } 1137 + }, 1138 + "node_modules/detect-libc": { 1139 + "version": "2.1.2", 1140 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", 1141 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", 1142 + "dev": true, 1143 + "license": "Apache-2.0", 1144 + "engines": { 1145 + "node": ">=8" 1146 + } 1147 + }, 1148 + "node_modules/error-stack-parser-es": { 1149 + "version": "1.0.5", 1150 + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", 1151 + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", 1152 + "dev": true, 1153 + "license": "MIT", 1154 + "funding": { 1155 + "url": "https://github.com/sponsors/antfu" 1156 + } 1157 + }, 1158 + "node_modules/esbuild": { 1159 + "version": "0.27.0", 1160 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", 1161 + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", 1162 + "dev": true, 1163 + "hasInstallScript": true, 1164 + "license": "MIT", 1165 + "bin": { 1166 + "esbuild": "bin/esbuild" 1167 + }, 1168 + "engines": { 1169 + "node": ">=18" 1170 + }, 1171 + "optionalDependencies": { 1172 + "@esbuild/aix-ppc64": "0.27.0", 1173 + "@esbuild/android-arm": "0.27.0", 1174 + "@esbuild/android-arm64": "0.27.0", 1175 + "@esbuild/android-x64": "0.27.0", 1176 + "@esbuild/darwin-arm64": "0.27.0", 1177 + "@esbuild/darwin-x64": "0.27.0", 1178 + "@esbuild/freebsd-arm64": "0.27.0", 1179 + "@esbuild/freebsd-x64": "0.27.0", 1180 + "@esbuild/linux-arm": "0.27.0", 1181 + "@esbuild/linux-arm64": "0.27.0", 1182 + "@esbuild/linux-ia32": "0.27.0", 1183 + "@esbuild/linux-loong64": "0.27.0", 1184 + "@esbuild/linux-mips64el": "0.27.0", 1185 + "@esbuild/linux-ppc64": "0.27.0", 1186 + "@esbuild/linux-riscv64": "0.27.0", 1187 + "@esbuild/linux-s390x": "0.27.0", 1188 + "@esbuild/linux-x64": "0.27.0", 1189 + "@esbuild/netbsd-arm64": "0.27.0", 1190 + "@esbuild/netbsd-x64": "0.27.0", 1191 + "@esbuild/openbsd-arm64": "0.27.0", 1192 + "@esbuild/openbsd-x64": "0.27.0", 1193 + "@esbuild/openharmony-arm64": "0.27.0", 1194 + "@esbuild/sunos-x64": "0.27.0", 1195 + "@esbuild/win32-arm64": "0.27.0", 1196 + "@esbuild/win32-ia32": "0.27.0", 1197 + "@esbuild/win32-x64": "0.27.0" 1198 + } 1199 + }, 1200 + "node_modules/exit-hook": { 1201 + "version": "2.2.1", 1202 + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", 1203 + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", 1204 + "dev": true, 1205 + "license": "MIT", 1206 + "engines": { 1207 + "node": ">=6" 1208 + }, 1209 + "funding": { 1210 + "url": "https://github.com/sponsors/sindresorhus" 1211 + } 1212 + }, 1213 + "node_modules/fsevents": { 1214 + "version": "2.3.3", 1215 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1216 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1217 + "dev": true, 1218 + "hasInstallScript": true, 1219 + "license": "MIT", 1220 + "optional": true, 1221 + "os": [ 1222 + "darwin" 1223 + ], 1224 + "engines": { 1225 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1226 + } 1227 + }, 1228 + "node_modules/glob-to-regexp": { 1229 + "version": "0.4.1", 1230 + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 1231 + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", 1232 + "dev": true, 1233 + "license": "BSD-2-Clause" 1234 + }, 1235 + "node_modules/is-arrayish": { 1236 + "version": "0.3.4", 1237 + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", 1238 + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", 1239 + "dev": true, 1240 + "license": "MIT" 1241 + }, 1242 + "node_modules/kleur": { 1243 + "version": "4.1.5", 1244 + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", 1245 + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", 1246 + "dev": true, 1247 + "license": "MIT", 1248 + "engines": { 1249 + "node": ">=6" 1250 + } 1251 + }, 1252 + "node_modules/mime": { 1253 + "version": "3.0.0", 1254 + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", 1255 + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", 1256 + "dev": true, 1257 + "license": "MIT", 1258 + "bin": { 1259 + "mime": "cli.js" 1260 + }, 1261 + "engines": { 1262 + "node": ">=10.0.0" 1263 + } 1264 + }, 1265 + "node_modules/miniflare": { 1266 + "version": "4.20251210.0", 1267 + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20251210.0.tgz", 1268 + "integrity": "sha512-k6kIoXwGVqlPZb0hcn+X7BmnK+8BjIIkusQPY22kCo2RaQJ/LzAjtxHQdGXerlHSnJyQivDQsL6BJHMpQfUFyw==", 1269 + "dev": true, 1270 + "license": "MIT", 1271 + "dependencies": { 1272 + "@cspotcode/source-map-support": "0.8.1", 1273 + "acorn": "8.14.0", 1274 + "acorn-walk": "8.3.2", 1275 + "exit-hook": "2.2.1", 1276 + "glob-to-regexp": "0.4.1", 1277 + "sharp": "^0.33.5", 1278 + "stoppable": "1.1.0", 1279 + "undici": "7.14.0", 1280 + "workerd": "1.20251210.0", 1281 + "ws": "8.18.0", 1282 + "youch": "4.1.0-beta.10", 1283 + "zod": "3.22.3" 1284 + }, 1285 + "bin": { 1286 + "miniflare": "bootstrap.js" 1287 + }, 1288 + "engines": { 1289 + "node": ">=18.0.0" 1290 + } 1291 + }, 1292 + "node_modules/path-to-regexp": { 1293 + "version": "6.3.0", 1294 + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", 1295 + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", 1296 + "dev": true, 1297 + "license": "MIT" 1298 + }, 1299 + "node_modules/pathe": { 1300 + "version": "2.0.3", 1301 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 1302 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 1303 + "dev": true, 1304 + "license": "MIT" 1305 + }, 1306 + "node_modules/semver": { 1307 + "version": "7.7.3", 1308 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", 1309 + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 1310 + "dev": true, 1311 + "license": "ISC", 1312 + "bin": { 1313 + "semver": "bin/semver.js" 1314 + }, 1315 + "engines": { 1316 + "node": ">=10" 1317 + } 1318 + }, 1319 + "node_modules/sharp": { 1320 + "version": "0.33.5", 1321 + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", 1322 + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", 1323 + "dev": true, 1324 + "hasInstallScript": true, 1325 + "license": "Apache-2.0", 1326 + "dependencies": { 1327 + "color": "^4.2.3", 1328 + "detect-libc": "^2.0.3", 1329 + "semver": "^7.6.3" 1330 + }, 1331 + "engines": { 1332 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1333 + }, 1334 + "funding": { 1335 + "url": "https://opencollective.com/libvips" 1336 + }, 1337 + "optionalDependencies": { 1338 + "@img/sharp-darwin-arm64": "0.33.5", 1339 + "@img/sharp-darwin-x64": "0.33.5", 1340 + "@img/sharp-libvips-darwin-arm64": "1.0.4", 1341 + "@img/sharp-libvips-darwin-x64": "1.0.4", 1342 + "@img/sharp-libvips-linux-arm": "1.0.5", 1343 + "@img/sharp-libvips-linux-arm64": "1.0.4", 1344 + "@img/sharp-libvips-linux-s390x": "1.0.4", 1345 + "@img/sharp-libvips-linux-x64": "1.0.4", 1346 + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", 1347 + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", 1348 + "@img/sharp-linux-arm": "0.33.5", 1349 + "@img/sharp-linux-arm64": "0.33.5", 1350 + "@img/sharp-linux-s390x": "0.33.5", 1351 + "@img/sharp-linux-x64": "0.33.5", 1352 + "@img/sharp-linuxmusl-arm64": "0.33.5", 1353 + "@img/sharp-linuxmusl-x64": "0.33.5", 1354 + "@img/sharp-wasm32": "0.33.5", 1355 + "@img/sharp-win32-ia32": "0.33.5", 1356 + "@img/sharp-win32-x64": "0.33.5" 1357 + } 1358 + }, 1359 + "node_modules/simple-swizzle": { 1360 + "version": "0.2.4", 1361 + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", 1362 + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", 1363 + "dev": true, 1364 + "license": "MIT", 1365 + "dependencies": { 1366 + "is-arrayish": "^0.3.1" 1367 + } 1368 + }, 1369 + "node_modules/stoppable": { 1370 + "version": "1.1.0", 1371 + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", 1372 + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", 1373 + "dev": true, 1374 + "license": "MIT", 1375 + "engines": { 1376 + "node": ">=4", 1377 + "npm": ">=6" 1378 + } 1379 + }, 1380 + "node_modules/supports-color": { 1381 + "version": "10.2.2", 1382 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", 1383 + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", 1384 + "dev": true, 1385 + "license": "MIT", 1386 + "engines": { 1387 + "node": ">=18" 1388 + }, 1389 + "funding": { 1390 + "url": "https://github.com/chalk/supports-color?sponsor=1" 1391 + } 1392 + }, 1393 + "node_modules/tslib": { 1394 + "version": "2.8.1", 1395 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 1396 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 1397 + "dev": true, 1398 + "license": "0BSD", 1399 + "optional": true 1400 + }, 1401 + "node_modules/undici": { 1402 + "version": "7.14.0", 1403 + "resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz", 1404 + "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==", 1405 + "dev": true, 1406 + "license": "MIT", 1407 + "engines": { 1408 + "node": ">=20.18.1" 1409 + } 1410 + }, 1411 + "node_modules/unenv": { 1412 + "version": "2.0.0-rc.24", 1413 + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", 1414 + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", 1415 + "dev": true, 1416 + "license": "MIT", 1417 + "dependencies": { 1418 + "pathe": "^2.0.3" 1419 + } 1420 + }, 1421 + "node_modules/workerd": { 1422 + "version": "1.20251210.0", 1423 + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251210.0.tgz", 1424 + "integrity": "sha512-9MUUneP1BnRE9XAYi94FXxHmiLGbO75EHQZsgWqSiOXjoXSqJCw8aQbIEPxCy19TclEl/kHUFYce8ST2W+Qpjw==", 1425 + "dev": true, 1426 + "hasInstallScript": true, 1427 + "license": "Apache-2.0", 1428 + "bin": { 1429 + "workerd": "bin/workerd" 1430 + }, 1431 + "engines": { 1432 + "node": ">=16" 1433 + }, 1434 + "optionalDependencies": { 1435 + "@cloudflare/workerd-darwin-64": "1.20251210.0", 1436 + "@cloudflare/workerd-darwin-arm64": "1.20251210.0", 1437 + "@cloudflare/workerd-linux-64": "1.20251210.0", 1438 + "@cloudflare/workerd-linux-arm64": "1.20251210.0", 1439 + "@cloudflare/workerd-windows-64": "1.20251210.0" 1440 + } 1441 + }, 1442 + "node_modules/wrangler": { 1443 + "version": "4.54.0", 1444 + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.54.0.tgz", 1445 + "integrity": "sha512-bANFsjDwJLbprYoBK+hUDZsVbUv2SqJd8QvArLIcZk+fPq4h/Ohtj5vkKXD3k0s2bD1DXLk08D+hYmeNH+xC6A==", 1446 + "dev": true, 1447 + "license": "MIT OR Apache-2.0", 1448 + "dependencies": { 1449 + "@cloudflare/kv-asset-handler": "0.4.1", 1450 + "@cloudflare/unenv-preset": "2.7.13", 1451 + "blake3-wasm": "2.1.5", 1452 + "esbuild": "0.27.0", 1453 + "miniflare": "4.20251210.0", 1454 + "path-to-regexp": "6.3.0", 1455 + "unenv": "2.0.0-rc.24", 1456 + "workerd": "1.20251210.0" 1457 + }, 1458 + "bin": { 1459 + "wrangler": "bin/wrangler.js", 1460 + "wrangler2": "bin/wrangler.js" 1461 + }, 1462 + "engines": { 1463 + "node": ">=20.0.0" 1464 + }, 1465 + "optionalDependencies": { 1466 + "fsevents": "~2.3.2" 1467 + }, 1468 + "peerDependencies": { 1469 + "@cloudflare/workers-types": "^4.20251210.0" 1470 + }, 1471 + "peerDependenciesMeta": { 1472 + "@cloudflare/workers-types": { 1473 + "optional": true 1474 + } 1475 + } 1476 + }, 1477 + "node_modules/ws": { 1478 + "version": "8.18.0", 1479 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 1480 + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 1481 + "dev": true, 1482 + "license": "MIT", 1483 + "engines": { 1484 + "node": ">=10.0.0" 1485 + }, 1486 + "peerDependencies": { 1487 + "bufferutil": "^4.0.1", 1488 + "utf-8-validate": ">=5.0.2" 1489 + }, 1490 + "peerDependenciesMeta": { 1491 + "bufferutil": { 1492 + "optional": true 1493 + }, 1494 + "utf-8-validate": { 1495 + "optional": true 1496 + } 1497 + } 1498 + }, 1499 + "node_modules/youch": { 1500 + "version": "4.1.0-beta.10", 1501 + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", 1502 + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", 1503 + "dev": true, 1504 + "license": "MIT", 1505 + "dependencies": { 1506 + "@poppinss/colors": "^4.1.5", 1507 + "@poppinss/dumper": "^0.6.4", 1508 + "@speed-highlight/core": "^1.2.7", 1509 + "cookie": "^1.0.2", 1510 + "youch-core": "^0.3.3" 1511 + } 1512 + }, 1513 + "node_modules/youch-core": { 1514 + "version": "0.3.3", 1515 + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", 1516 + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", 1517 + "dev": true, 1518 + "license": "MIT", 1519 + "dependencies": { 1520 + "@poppinss/exception": "^1.2.2", 1521 + "error-stack-parser-es": "^1.0.5" 1522 + } 1523 + }, 1524 + "node_modules/zod": { 1525 + "version": "3.22.3", 1526 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", 1527 + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", 1528 + "dev": true, 1529 + "license": "MIT", 1530 + "funding": { 1531 + "url": "https://github.com/sponsors/colinhacks" 1532 + } 1533 + } 1534 + } 1535 + }
+11
site/package.json
··· 1 + { 2 + "name": "leaflet-search-site", 3 + "type": "module", 4 + "scripts": { 5 + "dev": "wrangler pages dev --port 8788", 6 + "deploy": "wrangler pages deploy" 7 + }, 8 + "devDependencies": { 9 + "wrangler": "^4.0.0" 10 + } 11 + }
+4
site/wrangler.json
··· 1 + { 2 + "name": "leaflet-search", 3 + "pages_build_output_dir": "." 4 + }
+30
tap/fly.toml
··· 1 + app = 'leaflet-search-tap' 2 + primary_region = 'ewr' 3 + 4 + [build] 5 + image = 'ghcr.io/bluesky-social/indigo/tap:latest' 6 + 7 + [env] 8 + TAP_DATABASE_URL = 'sqlite:///data/tap.db' 9 + TAP_BIND = ':2480' 10 + TAP_SIGNAL_COLLECTION = 'pub.leaflet.document' 11 + TAP_COLLECTION_FILTERS = 'pub.leaflet.document,pub.leaflet.publication' 12 + TAP_DISABLE_ACKS = 'true' 13 + TAP_LOG_LEVEL = 'info' 14 + TAP_CURSOR_SAVE_INTERVAL = '5s' 15 + 16 + [http_service] 17 + internal_port = 2480 18 + force_https = false 19 + auto_stop_machines = 'off' 20 + auto_start_machines = true 21 + min_machines_running = 1 22 + 23 + [[vm]] 24 + memory = '2gb' 25 + cpu_kind = 'shared' 26 + cpus = 1 27 + 28 + [mounts] 29 + source = 'leaflet_tap_data' 30 + destination = '/data'
+13
tap/justfile
··· 1 + # tap instance for leaflet-search 2 + 3 + deploy: 4 + fly deploy --app leaflet-search-tap 5 + 6 + status: 7 + fly status --app leaflet-search-tap 8 + 9 + logs: 10 + fly logs --app leaflet-search-tap 11 + 12 + ssh: 13 + fly ssh console --app leaflet-search-tap