···1010bun run test:unit # unit tests (no Node needed)
1111mise run test # full suite (unit + integration, needs Node 22)
1212bun run lint # check with Biome
1313-bun run typecheck # tsc --noEmit
1313+bun run typecheck # tsgo --noEmit
1414```
15151616The database auto-seeds with sample data on first run. To reset, delete `lichen.db` and restart.
···5353 pages: "Pages",
5454 edit: "Modifier",
5555 export: "Exporter",
5656+ exportNote: "Note (.md)",
5757+ exportWiki: "Wiki (.zip)",
5658 },
5759 editor: {
5860 title: "Titre",
···147149 wikiLanguageRequired: "La langue est requise.",
148150 invalidZip: "Fichier zip invalide ou corrompu.",
149151 tooManyFiles: "Trop de fichiers dans le zip (max 100 fichiers markdown).",
150150- zipTooLarge: "Le contenu du zip depasse la limite de 50 Mo.",
151151- noMarkdownFiles: "Aucun fichier markdown trouve dans le zip.",
152152- importFailed: "Echec de l'importation : {error}",
152152+ zipTooLarge: "Le contenu du zip dépasse la limite de 50 Mo.",
153153+ noMarkdownFiles: "Aucun fichier markdown trouvé dans le zip.",
154154+ importFailed: "Échec de l'importation : {error}",
153155 },
154156};
···169169 "CREATE INDEX IF NOT EXISTS idx_blobs_revision ON blobs(revision_at_uri)",
170170 );
171171 db.run("CREATE INDEX IF NOT EXISTS idx_bookmarks_did ON bookmarks(did)");
172172+ db.run("CREATE INDEX IF NOT EXISTS idx_wikis_did ON wikis(did)");
172173}
+15-11
src/server/routes/blob.ts
···11import { existsSync, mkdirSync } from "node:fs";
22import { join, resolve } from "node:path";
33import { Elysia } from "elysia";
44+import { isAuthEnabled } from "../../atproto/env.ts";
45import { getAgent, getSessionFromRequest } from "../../atproto/session.ts";
56import { MIME_TO_EXT } from "../../lib/constants.ts";
67import { formatError } from "../../lib/errors.ts";
77-import { getIdResolver } from "../../lib/identity.ts";
88+import { resolvePdsEndpoint } from "../../lib/identity.ts";
89import { processImage } from "../../lib/image.ts";
9101011const LOCAL_BLOB_DIR = "data/blobs";
···4142 }
42434344 const session = await getSessionFromRequest(request);
4545+4646+ if (!session && isAuthEnabled()) {
4747+ return new Response(
4848+ JSON.stringify({ error: "Authentication required" }),
4949+ {
5050+ status: 401,
5151+ headers: { "Content-Type": "application/json" },
5252+ },
5353+ );
5454+ }
44554556 if (session) {
4657 // ATProto mode: upload to PDS
···100111101112 let pdsEndpoint: string;
102113 try {
103103- const didDoc = await getIdResolver().did.resolve(did);
104104- if (!didDoc) {
105105- return new Response("DID not found", { status: 404 });
106106- }
107107- const service = didDoc.service?.find(
108108- (s) =>
109109- s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer",
110110- );
111111- if (!service || typeof service.serviceEndpoint !== "string") {
114114+ const resolved = await resolvePdsEndpoint(did);
115115+ if (!resolved) {
112116 return new Response("PDS not found for DID", { status: 404 });
113117 }
114114- pdsEndpoint = service.serviceEndpoint;
118118+ pdsEndpoint = resolved;
115119 } catch {
116120 return new Response("Failed to resolve DID", { status: 502 });
117121 }
+1-10
src/server/routes/explore.ts
···11import { Elysia } from "elysia";
22import { resolveRequestContext } from "../../lib/access.ts";
33+import { parsePage, parseSort } from "../../lib/query-params.ts";
34import { htmlResponse } from "../../lib/response.ts";
45import { explorePage } from "../../views/explore.ts";
55-import type { WikiSort } from "../db/queries/index.ts";
66import {
77 getWikiLanguages,
88 listPublicWikisPaginated,
99} from "../db/queries/index.ts";
10101111const EXPLORE_LIMIT = 12;
1212-1313-function parseSort(value: unknown): WikiSort {
1414- return value === "created" ? "created" : "updated";
1515-}
1616-1717-function parsePage(value: unknown): number {
1818- const n = Number(value);
1919- return Number.isInteger(n) && n >= 1 ? n : 1;
2020-}
21122213export const exploreRoutes = new Elysia()
2314 .get("/explore", async ({ query, request }) => {