seemore 1.10.8 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -19,43 +19,43 @@ __export(paths_exports, {
19
19
  packageRoot: () => packageRoot,
20
20
  resolveContentRoot: () => resolveContentRoot
21
21
  });
22
- import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "fs";
23
- import { createHash as createHash2 } from "crypto";
22
+ import { existsSync, readFileSync, realpathSync } from "fs";
23
+ import { createHash } from "crypto";
24
24
  import { tmpdir } from "os";
25
- import { dirname as dirname3, join as join2, resolve as resolve3 } from "path";
25
+ import { dirname as dirname2, join as join3, resolve } from "path";
26
26
  import { fileURLToPath } from "url";
27
27
  function packageRoot() {
28
- let dir = dirname3(fileURLToPath(import.meta.url));
28
+ let dir = dirname2(fileURLToPath(import.meta.url));
29
29
  for (let depth = 0; depth < 10; depth++) {
30
- if (existsSync2(join2(dir, "package.json"))) return dir;
31
- const parent = dirname3(dir);
30
+ if (existsSync(join3(dir, "package.json"))) return dir;
31
+ const parent = dirname2(dir);
32
32
  if (parent === dir) break;
33
33
  dir = parent;
34
34
  }
35
35
  throw new Error("seemore: could not locate its own package root.");
36
36
  }
37
37
  function appRoot() {
38
- return join2(packageRoot(), "src", "app");
38
+ return join3(packageRoot(), "src", "app");
39
39
  }
40
40
  function packageDirOf(name, fromFile) {
41
- let dir = dirname3(fromFile);
41
+ let dir = dirname2(fromFile);
42
42
  for (let depth = 0; depth < 10; depth++) {
43
- const manifest = join2(dir, "package.json");
44
- if (existsSync2(manifest) && JSON.parse(readFileSync2(manifest, "utf8")).name === name) {
43
+ const manifest = join3(dir, "package.json");
44
+ if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === name) {
45
45
  return dir;
46
46
  }
47
- const parent = dirname3(dir);
47
+ const parent = dirname2(dir);
48
48
  if (parent === dir) break;
49
49
  dir = parent;
50
50
  }
51
51
  throw new Error(`seemore: could not locate the "${name}" package directory.`);
52
52
  }
53
53
  function cacheDir(contentRoot) {
54
- const key = createHash2("sha256").update(resolve3(contentRoot)).digest("hex").slice(0, 12);
55
- return join2(tmpdir(), "seemore", key);
54
+ const key = createHash("sha256").update(resolve(contentRoot)).digest("hex").slice(0, 12);
55
+ return join3(tmpdir(), "seemore", key);
56
56
  }
57
57
  function resolveContentRoot(cwd, explicit) {
58
- return explicit !== void 0 ? canonicalise(resolve3(cwd, explicit)) : canonicalise(cwd);
58
+ return explicit !== void 0 ? canonicalise(resolve(cwd, explicit)) : canonicalise(cwd);
59
59
  }
60
60
  function canonicalise(dir) {
61
61
  try {
@@ -75,18 +75,102 @@ import { parseArgs } from "util";
75
75
  import pc5 from "picocolors";
76
76
 
77
77
  // src/cli/build.ts
78
- import { mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync5 } from "fs";
79
- import { createRequire as createRequire3 } from "module";
80
- import { tmpdir as tmpdir2 } from "os";
81
- import { isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve as resolve5 } from "path";
78
+ import { mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
79
+ import { createRequire as createRequire4 } from "module";
80
+ import { tmpdir as tmpdir3 } from "os";
81
+ import { isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve as resolve5 } from "path";
82
82
  import pc2 from "picocolors";
83
83
  import { build as viteBuild } from "vite";
84
84
 
85
- // src/node/config/load.ts
86
- import { existsSync } from "fs";
87
- import { dirname, isAbsolute, resolve } from "path";
88
- import { createJiti } from "jiti";
89
- import "zod";
85
+ // src/node/auth/build.ts
86
+ import { writeFileSync as writeFileSync4 } from "fs";
87
+ import { join as join7 } from "path";
88
+
89
+ // src/shared/auth/crypto.ts
90
+ var KDF_ITERATIONS = 6e5;
91
+ var MAGIC = new Uint8Array([83, 77, 80, 49]);
92
+ var IV_BYTES = 12;
93
+ var HEADER_BYTES = MAGIC.length + IV_BYTES;
94
+ var SALT_PREFIX = "seemore-auth-v1\0";
95
+ var encoder = new TextEncoder();
96
+ function subtle() {
97
+ return globalThis.crypto.subtle;
98
+ }
99
+ function normalisePassword(password) {
100
+ return password.normalize("NFC");
101
+ }
102
+ async function deriveSalt(id) {
103
+ return new Uint8Array(await subtle().digest("SHA-256", encoder.encode(SALT_PREFIX + id)));
104
+ }
105
+ async function deriveKek(password, salt, iterations = KDF_ITERATIONS) {
106
+ const material = await subtle().importKey("raw", encoder.encode(normalisePassword(password)), "PBKDF2", false, [
107
+ "deriveKey"
108
+ ]);
109
+ return await subtle().deriveKey(
110
+ { name: "PBKDF2", hash: "SHA-256", salt, iterations },
111
+ material,
112
+ { name: "AES-KW", length: 256 },
113
+ false,
114
+ ["wrapKey", "unwrapKey"]
115
+ );
116
+ }
117
+ async function generateContentKey() {
118
+ return await subtle().generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
119
+ }
120
+ async function wrapContentKey(contentKey, kek) {
121
+ return new Uint8Array(await subtle().wrapKey("raw", contentKey, kek, "AES-KW"));
122
+ }
123
+ async function encryptFile(key, path, plaintext) {
124
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
125
+ const sealed = await subtle().encrypt(
126
+ { name: "AES-GCM", iv, additionalData: encoder.encode(path), tagLength: 128 },
127
+ key,
128
+ plaintext
129
+ );
130
+ const out = new Uint8Array(HEADER_BYTES + sealed.byteLength);
131
+ out.set(MAGIC, 0);
132
+ out.set(iv, MAGIC.length);
133
+ out.set(new Uint8Array(sealed), HEADER_BYTES);
134
+ return out;
135
+ }
136
+ function isEncrypted(bytes) {
137
+ return bytes.length > HEADER_BYTES && MAGIC.every((byte, index) => bytes[index] === byte);
138
+ }
139
+ async function createManifest(options) {
140
+ const iterations = options.iterations ?? KDF_ITERATIONS;
141
+ const salt = await deriveSalt(options.id);
142
+ const kek = await deriveKek(options.password, salt, iterations);
143
+ const contentKey = await generateContentKey();
144
+ return {
145
+ contentKey,
146
+ manifest: {
147
+ v: 1,
148
+ kdf: { name: "PBKDF2", hash: "SHA-256", iterations, salt: encodeBase64(salt) },
149
+ key: encodeBase64(await wrapContentKey(contentKey, kek)),
150
+ remember: options.remember
151
+ }
152
+ };
153
+ }
154
+ function encodeBase64(bytes) {
155
+ let binary = "";
156
+ for (const byte of bytes) binary += String.fromCharCode(byte);
157
+ return btoa(binary);
158
+ }
159
+
160
+ // src/shared/auth/files.ts
161
+ var PUBLIC_FILES = [
162
+ "index.html",
163
+ "404.html",
164
+ "200.html",
165
+ "_redirects",
166
+ ".nojekyll",
167
+ "_headers",
168
+ "sw.js",
169
+ "auth.json"
170
+ ];
171
+ var MANIFEST_FILE = "auth.json";
172
+ var WORKER_FILE = "sw.js";
173
+ var APP_FILE = "app.html";
90
174
 
91
175
  // src/shared/base.ts
92
176
  var EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
@@ -114,6 +198,386 @@ function withBase(base, href) {
114
198
  return b + href.replace(/^\/+/, "");
115
199
  }
116
200
 
201
+ // src/node/content/slug.ts
202
+ import { slug as slugify } from "github-slugger";
203
+ var INDEX_NAMES = /* @__PURE__ */ new Set(["index", "readme"]);
204
+ var CONTENT_EXT = /\.mdx?$/i;
205
+ function toPosix(file) {
206
+ return file.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
207
+ }
208
+ function slugifySegment(segment) {
209
+ const slugged = slugify(segment);
210
+ if (slugged !== "") return slugged;
211
+ const fallback = segment.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
212
+ return fallback === "" ? "untitled" : fallback;
213
+ }
214
+ function toRoute(file) {
215
+ const posix = toPosix(file);
216
+ const segments = posix.split("/");
217
+ const basename3 = segments.pop() ?? "";
218
+ const stem = basename3.replace(CONTENT_EXT, "");
219
+ const isIndex = INDEX_NAMES.has(stem.toLowerCase());
220
+ const slugs = segments.map(slugifySegment);
221
+ if (!isIndex) slugs.push(slugifySegment(stem));
222
+ return {
223
+ file: posix,
224
+ url: slugs.length === 0 ? "/" : `/${slugs.join("/")}`,
225
+ slugs,
226
+ output: [...slugs, "index.html"].join("/"),
227
+ isIndex
228
+ };
229
+ }
230
+ function resolveRoutes(files) {
231
+ const sorted = [...files].map(toPosix).sort();
232
+ const byUrl = /* @__PURE__ */ new Map();
233
+ const warnings = [];
234
+ const errors = [];
235
+ for (const file of sorted) {
236
+ const route = toRoute(file);
237
+ const bucket = byUrl.get(route.url);
238
+ if (bucket) bucket.push(route);
239
+ else byUrl.set(route.url, [route]);
240
+ }
241
+ const routes = [];
242
+ for (const [url, candidates] of [...byUrl.entries()].sort(([a], [b]) => a < b ? -1 : 1)) {
243
+ if (candidates.length === 1) {
244
+ routes.push(candidates[0]);
245
+ continue;
246
+ }
247
+ const indexes = candidates.filter((c) => c.isIndex);
248
+ if (indexes.length === candidates.length) {
249
+ const winner = indexes.find((c) => c.file.split("/").pop()?.toLowerCase().startsWith("index")) ?? indexes[0];
250
+ const losers = indexes.filter((c) => c !== winner);
251
+ warnings.push(
252
+ `${url} has more than one index file: using ${winner.file}, ignoring ${losers.map((l) => l.file).join(", ")}.`
253
+ );
254
+ routes.push(winner);
255
+ continue;
256
+ }
257
+ errors.push(
258
+ `Duplicate route ${url} produced by ${candidates.length} files:
259
+ ` + candidates.map((c) => ` - ${c.file}`).join("\n") + `
260
+ Rename one of them, or exclude it with \`exclude\` in seemore.config.ts.`
261
+ );
262
+ }
263
+ return { routes, errors, warnings };
264
+ }
265
+
266
+ // src/node/prerender/deploy.ts
267
+ import { writeFileSync } from "fs";
268
+ import { join } from "path";
269
+ function writeDeployArtifacts(outDir, base, shell, options = {}) {
270
+ const prefix = base === "/" ? "" : base.replace(/\/+$/, "");
271
+ writeFileSync(join(outDir, "_redirects"), `${prefix}/* ${prefix}/index.html 200
272
+ `, "utf8");
273
+ writeFileSync(join(outDir, "200.html"), shell, "utf8");
274
+ writeFileSync(join(outDir, ".nojekyll"), "", "utf8");
275
+ if (options.auth === true) {
276
+ writeFileSync(join(outDir, "_headers"), "/*\n X-Robots-Tag: noindex, nofollow\n", "utf8");
277
+ }
278
+ }
279
+
280
+ // src/node/prerender/emit.ts
281
+ import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
282
+ import { dirname, join as join2 } from "path";
283
+ function outputPathFor(url) {
284
+ const clean = url.replace(/^\/+|\/+$/g, "");
285
+ return clean === "" ? "index.html" : join2(clean, "index.html");
286
+ }
287
+ function writeHtml(outDir, relativePath, html) {
288
+ const target = join2(outDir, relativePath);
289
+ mkdirSync(dirname(target), { recursive: true });
290
+ writeFileSync2(target, html, "utf8");
291
+ }
292
+ function applyTemplate(template, { html, head }) {
293
+ return template.replace("<!--seemore-head-->", () => head).replace("<!--seemore-app-->", () => html);
294
+ }
295
+
296
+ // src/node/auth/bundle.ts
297
+ init_paths();
298
+ import { tmpdir as tmpdir2 } from "os";
299
+ import { join as join4 } from "path";
300
+ import { build } from "vite";
301
+ var bundles = /* @__PURE__ */ new Map();
302
+ function bundleAuthScript(entry) {
303
+ let bundle = bundles.get(entry);
304
+ if (bundle === void 0) {
305
+ bundle = compile(entry);
306
+ bundles.set(entry, bundle);
307
+ bundle.catch(() => bundles.delete(entry));
308
+ }
309
+ return bundle;
310
+ }
311
+ async function compile(entry) {
312
+ const result = await build({
313
+ root: packageRoot(),
314
+ configFile: false,
315
+ envDir: false,
316
+ publicDir: false,
317
+ clearScreen: false,
318
+ logLevel: "warn",
319
+ cacheDir: join4(tmpdir2(), "seemore", "auth-scripts"),
320
+ build: {
321
+ write: false,
322
+ minify: true,
323
+ target: "es2020",
324
+ copyPublicDir: false,
325
+ reportCompressedSize: false,
326
+ lib: {
327
+ entry: join4(packageRoot(), "src", "shared", "auth", `${entry}.ts`),
328
+ formats: ["iife"],
329
+ name: "seemoreAuth",
330
+ fileName: () => `${entry}.js`
331
+ }
332
+ }
333
+ });
334
+ const outputs = Array.isArray(result) ? result : [result];
335
+ const chunk = outputs.flatMap((output) => output.output ?? []).find((file) => file.type === "chunk");
336
+ if (chunk?.code === void 0) throw new Error(`seemore: bundling the ${entry} script produced no code.`);
337
+ return chunk.code;
338
+ }
339
+
340
+ // src/node/auth/output.ts
341
+ import { readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
342
+ import { join as join5 } from "path";
343
+ function publicFiles(favicon) {
344
+ return favicon === void 0 ? [...PUBLIC_FILES] : [...PUBLIC_FILES, toPosix(favicon)];
345
+ }
346
+ async function encryptOutput(outDir, contentKey, open) {
347
+ const isPublic = new Set(open);
348
+ let encrypted = 0;
349
+ for (const path of listFiles(outDir)) {
350
+ if (isPublic.has(path)) continue;
351
+ const file = join5(outDir, path);
352
+ writeFileSync3(file, await encryptFile(contentKey, path, new Uint8Array(readFileSync2(file))));
353
+ encrypted++;
354
+ }
355
+ return encrypted;
356
+ }
357
+ function assertOutputEncrypted(outDir, open) {
358
+ const isPublic = new Set(open);
359
+ const plaintext = listFiles(outDir).filter((path) => !isPublic.has(path) && !isEncrypted(readFileSync2(join5(outDir, path))));
360
+ if (plaintext.length === 0) return;
361
+ throw new Error(
362
+ `seemore stopped the build: ${plaintext.length === 1 ? "this file was" : "these files were"} about to ship unencrypted, and \`auth\` only allows its public files in plain text:
363
+ ${plaintext.map((path) => ` - ${path}`).join("\n")}
364
+
365
+ This is a bug in seemore. Please report it at https://github.com/arifszn/seemore/issues.`
366
+ );
367
+ }
368
+ function listFiles(dir, prefix = "") {
369
+ const files = [];
370
+ for (const entry of readdirSync(join5(dir, prefix), { withFileTypes: true })) {
371
+ const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
372
+ if (entry.isDirectory()) files.push(...listFiles(dir, path));
373
+ else files.push(path);
374
+ }
375
+ return files.sort();
376
+ }
377
+
378
+ // src/node/auth/shell.ts
379
+ function renderLockShell(options) {
380
+ const title = escapeHtml(options.title);
381
+ const icon = options.favicon === void 0 ? void 0 : escapeHtml(options.favicon);
382
+ const config = JSON.stringify({ base: options.base }).replace(/</g, "\\u003c");
383
+ return [
384
+ "<!doctype html>",
385
+ '<html lang="en">',
386
+ "<head>",
387
+ '<meta charset="utf-8" />',
388
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
389
+ // Search engines: nothing past this screen is readable to them anyway.
390
+ '<meta name="robots" content="noindex, nofollow" />',
391
+ `<title>${title}</title>`,
392
+ options.description === void 0 ? "" : `<meta name="description" content="${escapeHtml(options.description)}" />`,
393
+ icon === void 0 ? "" : `<link rel="icon" href="${icon}" />`,
394
+ `<style>${styles(options.colours)}</style>`,
395
+ `<script>${inlineScript(THEME_SCRIPT)}</script>`,
396
+ "</head>",
397
+ "<body>",
398
+ '<main class="lock">',
399
+ icon === void 0 ? "" : `<img class="lock-icon" src="${icon}" alt="" width="44" height="44" />`,
400
+ `<h1>${title}</h1>`,
401
+ options.description === void 0 ? "" : `<p class="lock-desc">${escapeHtml(options.description)}</p>`,
402
+ '<form id="seemore-auth-form" hidden>',
403
+ '<label class="lock-label" for="seemore-auth-password">Password</label>',
404
+ '<input id="seemore-auth-password" name="password" type="password" placeholder="Password" autocomplete="current-password" required />',
405
+ '<button id="seemore-auth-submit" type="submit">Unlock</button>',
406
+ '<p id="seemore-auth-status" class="lock-status" role="alert"></p>',
407
+ "</form>",
408
+ `<p id="seemore-auth-unsupported" class="lock-note" hidden>This browser can't unlock this site \u2014 it needs service workers. Try a regular (not private) window or another browser.</p>`,
409
+ '<noscript><p class="lock-note">This site needs JavaScript to unlock.</p></noscript>',
410
+ "</main>",
411
+ `<script type="application/json" id="seemore-auth-config">${config}</script>`,
412
+ `<script>${inlineScript(options.script)}</script>`,
413
+ "</body>",
414
+ "</html>",
415
+ ""
416
+ ].filter((line) => line !== "").join("\n");
417
+ }
418
+ var THEME_SCRIPT = "try{var t=localStorage.getItem('theme');if(t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark')}catch(e){}";
419
+ function styles(colours) {
420
+ const vars = (tokens) => Object.entries(tokens).map(([name, value]) => `--${name}:${value}`).join(";");
421
+ return [
422
+ `:root{color-scheme:light;${vars(colours.light)}}`,
423
+ `:root.dark{color-scheme:dark;${vars(colours.dark)}}`,
424
+ "*{box-sizing:border-box}",
425
+ "[hidden]{display:none!important}",
426
+ "html,body{min-height:100%}",
427
+ 'body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:var(--background);color:var(--foreground);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}',
428
+ ".lock{width:100%;max-width:360px;padding:32px;border:1px solid color-mix(in srgb,var(--foreground) 12%,transparent);border-radius:14px;background:color-mix(in srgb,var(--background) 94%,var(--foreground));box-shadow:0 12px 32px color-mix(in srgb,var(--foreground) 8%,transparent);text-align:center;animation:lock-in .24s ease-out both}",
429
+ ":root:not(.dark) .lock{background:color-mix(in srgb,var(--background) 92%,white)}",
430
+ ".lock-icon{width:40px;height:40px;margin-bottom:20px}",
431
+ "h1{margin:0;font-size:22px;line-height:1.2;font-weight:650;letter-spacing:-.015em}",
432
+ ".lock-site{margin:7px 0 0;color:var(--muted-foreground);font-size:14px}",
433
+ ".lock-desc{margin:6px 0 0;font-size:14px;color:var(--muted-foreground)}",
434
+ "form{width:100%;margin-top:24px;text-align:left}",
435
+ ".lock-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}",
436
+ "input{display:block;width:100%;min-width:0;font:inherit;padding:10px 12px;border-radius:8px;border:1px solid var(--border);background:var(--background);color:inherit}",
437
+ "input:focus{outline:2px solid var(--ring);outline-offset:2px}",
438
+ "button{display:block;width:100%;margin-top:18px;font:inherit;font-weight:600;padding:10px 16px;border:0;border-radius:8px;background:var(--primary);color:var(--primary-foreground);cursor:pointer}",
439
+ "button:disabled{opacity:.6;cursor:progress}",
440
+ ".lock-status{margin:10px 0 0;min-height:1.5em;font-size:14px;color:var(--error)}",
441
+ ".lock-note{margin:14px 0 0;font-size:14px;color:var(--muted-foreground);max-width:34ch}",
442
+ ".lock-shake{animation:lock-shake .3s ease-in-out}",
443
+ "@keyframes lock-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}",
444
+ "@keyframes lock-shake{20%,60%{transform:translateX(-5px)}40%,80%{transform:translateX(5px)}}",
445
+ "@media (max-width:400px){.lock{padding:24px}}",
446
+ "@media (prefers-reduced-motion:reduce){.lock,.lock-shake{animation:none}}"
447
+ ].join("\n");
448
+ }
449
+ function inlineScript(code) {
450
+ return code.replace(/<\/(script)/gi, "<\\/$1");
451
+ }
452
+ function escapeHtml(value) {
453
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
454
+ }
455
+
456
+ // src/node/auth/theme.ts
457
+ import { readFileSync as readFileSync3 } from "fs";
458
+ import { createRequire } from "module";
459
+ import { dirname as dirname3, join as join6 } from "path";
460
+ var TOKENS = [
461
+ "background",
462
+ "foreground",
463
+ "muted-foreground",
464
+ "card",
465
+ "border",
466
+ "primary",
467
+ "primary-foreground",
468
+ "ring",
469
+ "error"
470
+ ];
471
+ var FALLBACK = {
472
+ background: "hsl(0, 0%, 96%)",
473
+ foreground: "hsl(0, 0%, 3.9%)",
474
+ "muted-foreground": "hsl(0, 0%, 45.1%)",
475
+ card: "hsl(0, 0%, 94.7%)",
476
+ border: "hsla(0, 0%, 80%, 50%)",
477
+ primary: "hsl(0, 0%, 9%)",
478
+ "primary-foreground": "hsl(0, 0%, 98%)",
479
+ ring: "hsl(0, 0%, 63.9%)",
480
+ error: "oklch(63.7% 0.237 25.331)"
481
+ };
482
+ var require_ = createRequire(import.meta.url);
483
+ function shellColours(theme) {
484
+ const light = {};
485
+ const dark = {};
486
+ let dir;
487
+ try {
488
+ dir = join6(dirname3(require_.resolve("fumadocs-ui/package.json")), "css");
489
+ } catch {
490
+ dir = void 0;
491
+ }
492
+ for (const file of dir === void 0 ? [] : [join6(dir, "lib", "default-colors.css"), join6(dir, `${theme}.css`)]) {
493
+ let css;
494
+ try {
495
+ css = readFileSync3(file, "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
496
+ } catch {
497
+ continue;
498
+ }
499
+ for (const [, prelude = "", block = ""] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
500
+ const selectors = (prelude.split(";").pop() ?? "").split(",").map((selector) => selector.trim());
501
+ const targets = [];
502
+ if (selectors.some((selector) => selector.startsWith("@theme") || selector === ":root")) targets.push(light);
503
+ if (selectors.includes(".dark")) targets.push(dark);
504
+ for (const [, name = "", value = ""] of block.matchAll(/--color-fd-([\w-]+)\s*:\s*([^;]+);/g)) {
505
+ if (value.includes("var(")) continue;
506
+ for (const target of targets) target[name] = value.trim();
507
+ }
508
+ }
509
+ }
510
+ const pick = (tokens, fallback) => Object.fromEntries(TOKENS.map((token) => [token, tokens[token] ?? fallback[token] ?? FALLBACK[token]]));
511
+ return { light: pick(light, {}), dark: pick(dark, light) };
512
+ }
513
+
514
+ // src/node/auth/build.ts
515
+ async function sealSite(ctx, outDir, template, password) {
516
+ const { config } = ctx;
517
+ const auth = config.auth;
518
+ if (auth === void 0) throw new Error("seemore: sealSite needs `auth` in the config.");
519
+ writeHtml(outDir, APP_FILE, applyTemplate(template, { html: "", head: appHead(config, template) }));
520
+ const shell = renderLockShell({
521
+ title: config.title,
522
+ description: config.description,
523
+ base: config.base,
524
+ favicon: faviconHref(config, template),
525
+ colours: shellColours(config.theme),
526
+ script: await bundleAuthScript("lock")
527
+ });
528
+ writeHtml(outDir, "index.html", shell);
529
+ writeHtml(outDir, "404.html", shell);
530
+ writeDeployArtifacts(outDir, config.base, shell, { auth: true });
531
+ const { manifest, contentKey } = await createManifest({ password, id: auth.id, remember: auth.remember });
532
+ writeFileSync4(join7(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
533
+ `, "utf8");
534
+ const open = publicFiles(config.favicon);
535
+ writeFileSync4(
536
+ join7(outDir, WORKER_FILE),
537
+ `var SEEMORE_AUTH=${JSON.stringify({ publicFiles: open })};
538
+ ${await bundleAuthScript("sw")}`,
539
+ "utf8"
540
+ );
541
+ const encrypted = await encryptOutput(outDir, contentKey, open);
542
+ assertOutputEncrypted(outDir, open);
543
+ return encrypted;
544
+ }
545
+ function faviconHref(config, template) {
546
+ if (config.favicon !== void 0) return withBase(config.base, `/${toPosix(config.favicon)}`);
547
+ return /<link rel="icon" href="(data:[^"]+)"/.exec(template)?.[1];
548
+ }
549
+ function appHead(config, template) {
550
+ const tags = [`<title>${escapeHtml(config.title)}</title>`, '<meta name="robots" content="noindex, nofollow" />'];
551
+ if (config.description !== void 0) {
552
+ tags.push(`<meta name="description" content="${escapeHtml(config.description)}" />`);
553
+ }
554
+ const favicon = faviconHref(config, template);
555
+ if (config.favicon !== void 0 && favicon !== void 0) tags.push(`<link rel="icon" href="${escapeHtml(favicon)}" />`);
556
+ return tags.join("\n ");
557
+ }
558
+
559
+ // src/node/auth/password.ts
560
+ var PASSWORD_ENV = "SEEMORE_PASSWORD";
561
+ function readPassword(command, env = process.env) {
562
+ const raw = env[PASSWORD_ENV];
563
+ if (raw === void 0 || raw === "") {
564
+ throw new Error(
565
+ `\`auth\` is on, but ${PASSWORD_ENV} is not set. Pass the password through the environment \u2014 in CI, from a secret:
566
+
567
+ ${PASSWORD_ENV}='a-long-passphrase' ${command}
568
+
569
+ Never put it in seemore.config.ts.`
570
+ );
571
+ }
572
+ return normalisePassword(raw);
573
+ }
574
+
575
+ // src/node/config/load.ts
576
+ import { existsSync as existsSync2 } from "fs";
577
+ import { dirname as dirname4, isAbsolute, resolve as resolve2 } from "path";
578
+ import { createJiti } from "jiti";
579
+ import "zod";
580
+
117
581
  // src/shared/types.ts
118
582
  var FEATURES = [
119
583
  "navigation.instant.prefetch",
@@ -251,6 +715,14 @@ var searchSchema = z.union([
251
715
  })
252
716
  ]);
253
717
  var pageActionsSchema = z.array(z.enum(ACTION_IDS)).default(["copy-markdown", "export-html"]);
718
+ var REMEMBER_FORMS = "expected a number of hours or days, like '12h' or '7d'.";
719
+ var authSchema = z.preprocess(
720
+ (input) => input === true ? {} : input === false ? void 0 : input,
721
+ z.strictObject({
722
+ id: z.string().trim().min(1, { error: "must not be empty. Leave `id` out to use the site title, or give the site a stable name." }).optional(),
723
+ remember: z.string({ error: REMEMBER_FORMS }).regex(/^[1-9]\d*[hd]$/, { error: REMEMBER_FORMS }).optional()
724
+ }).optional()
725
+ );
254
726
  var configSchema = z.object({
255
727
  /**
256
728
  * Optional here, but required whenever a config file exists — load.ts enforces that,
@@ -277,7 +749,8 @@ var configSchema = z.object({
277
749
  }).optional(),
278
750
  search: searchSchema.default("static"),
279
751
  pageActions: pageActionsSchema,
280
- exclude: z.array(z.string()).default([])
752
+ exclude: z.array(z.string()).default([]),
753
+ auth: authSchema
281
754
  });
282
755
 
283
756
  // src/node/config/load.ts
@@ -289,9 +762,11 @@ function resolveConfig(input, options) {
289
762
  // Nothing to link to without an edit base, so the flag follows the option.
290
763
  "content.action.edit": parsed.editLink !== void 0
291
764
  });
765
+ const title = parsed.title ?? "Docs";
766
+ const auth = parsed.auth === void 0 ? void 0 : { id: parsed.auth.id ?? title, remember: rememberSeconds(parsed.auth.remember) };
767
+ if (auth !== void 0) assertAuthCompatible(features, search);
292
768
  return {
293
- // Only reached without a config file (see parseOrThrow): 'Docs' is the best name we can know.
294
- title: parsed.title ?? "Docs",
769
+ title,
295
770
  description: parsed.description,
296
771
  favicon: parsed.favicon,
297
772
  base: normaliseBase(parsed.base),
@@ -304,10 +779,32 @@ function resolveConfig(input, options) {
304
779
  search,
305
780
  pageActions: parsed.pageActions,
306
781
  exclude: parsed.exclude,
782
+ auth,
307
783
  root: options.root,
308
784
  configFile: options.configFile
309
785
  };
310
786
  }
787
+ var DAY_SECONDS = 86400;
788
+ function rememberSeconds(remember) {
789
+ if (remember === void 0) return DAY_SECONDS;
790
+ const amount = Number(remember.slice(0, -1));
791
+ return remember.endsWith("h") ? amount * 3600 : amount * DAY_SECONDS;
792
+ }
793
+ function assertAuthCompatible(features, search) {
794
+ const problems = [];
795
+ if (features["social.cards"]) {
796
+ problems.push("`auth` cannot be combined with `social.cards`: the cards would publish page titles as public images. Remove one of them.");
797
+ }
798
+ if (search.provider !== "static") {
799
+ problems.push(
800
+ `\`auth\` cannot be combined with \`search.provider: '${search.provider}'\`: the search index would go to a third party in plaintext. Use \`search: 'static'\`, or remove \`auth\`.`
801
+ );
802
+ }
803
+ if (problems.length > 0) {
804
+ throw new Error(`Incompatible options in seemore config:
805
+ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
806
+ }
807
+ }
311
808
  async function loadConfig(options) {
312
809
  const file = findConfigFile(options);
313
810
  if (file === void 0) {
@@ -327,21 +824,21 @@ ${error instanceof Error ? error.message : String(error)}`, {
327
824
  throw new Error(`${file} must export a config object as its default export, got ${typeof loaded}.`);
328
825
  }
329
826
  return {
330
- config: resolveConfig(loaded, { root: dirname(file), configFile: file }),
827
+ config: resolveConfig(loaded, { root: dirname4(file), configFile: file }),
331
828
  file
332
829
  };
333
830
  }
334
831
  function findConfigFile({ root, configPath }) {
335
832
  if (configPath !== void 0) {
336
833
  const absolute = resolveFrom(root, configPath);
337
- if (!existsSync(absolute)) {
834
+ if (!existsSync2(absolute)) {
338
835
  throw new Error(`Config file not found: ${absolute}`);
339
836
  }
340
837
  return absolute;
341
838
  }
342
839
  for (const name of CONFIG_NAMES) {
343
- const candidate = resolve(root, name);
344
- if (existsSync(candidate)) return candidate;
840
+ const candidate = resolve2(root, name);
841
+ if (existsSync2(candidate)) return candidate;
345
842
  }
346
843
  return void 0;
347
844
  }
@@ -349,7 +846,7 @@ function resolveConfigPath(options) {
349
846
  return options.configPath === void 0 ? void 0 : resolveFrom(options.cwd, options.configPath);
350
847
  }
351
848
  function resolveFrom(root, path) {
352
- return isAbsolute(path) ? path : resolve(root, path);
849
+ return isAbsolute(path) ? path : resolve2(root, path);
353
850
  }
354
851
  function parseOrThrow(input, file) {
355
852
  const result = configSchema.safeParse(input);
@@ -383,73 +880,6 @@ function explain(issue) {
383
880
 
384
881
  // src/node/content/links.ts
385
882
  import { slug as slugify2 } from "github-slugger";
386
-
387
- // src/node/content/slug.ts
388
- import { slug as slugify } from "github-slugger";
389
- var INDEX_NAMES = /* @__PURE__ */ new Set(["index", "readme"]);
390
- var CONTENT_EXT = /\.mdx?$/i;
391
- function toPosix(file) {
392
- return file.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
393
- }
394
- function slugifySegment(segment) {
395
- const slugged = slugify(segment);
396
- if (slugged !== "") return slugged;
397
- const fallback = segment.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
398
- return fallback === "" ? "untitled" : fallback;
399
- }
400
- function toRoute(file) {
401
- const posix = toPosix(file);
402
- const segments = posix.split("/");
403
- const basename3 = segments.pop() ?? "";
404
- const stem = basename3.replace(CONTENT_EXT, "");
405
- const isIndex = INDEX_NAMES.has(stem.toLowerCase());
406
- const slugs = segments.map(slugifySegment);
407
- if (!isIndex) slugs.push(slugifySegment(stem));
408
- return {
409
- file: posix,
410
- url: slugs.length === 0 ? "/" : `/${slugs.join("/")}`,
411
- slugs,
412
- output: [...slugs, "index.html"].join("/"),
413
- isIndex
414
- };
415
- }
416
- function resolveRoutes(files) {
417
- const sorted = [...files].map(toPosix).sort();
418
- const byUrl = /* @__PURE__ */ new Map();
419
- const warnings = [];
420
- const errors = [];
421
- for (const file of sorted) {
422
- const route = toRoute(file);
423
- const bucket = byUrl.get(route.url);
424
- if (bucket) bucket.push(route);
425
- else byUrl.set(route.url, [route]);
426
- }
427
- const routes = [];
428
- for (const [url, candidates] of [...byUrl.entries()].sort(([a], [b]) => a < b ? -1 : 1)) {
429
- if (candidates.length === 1) {
430
- routes.push(candidates[0]);
431
- continue;
432
- }
433
- const indexes = candidates.filter((c) => c.isIndex);
434
- if (indexes.length === candidates.length) {
435
- const winner = indexes.find((c) => c.file.split("/").pop()?.toLowerCase().startsWith("index")) ?? indexes[0];
436
- const losers = indexes.filter((c) => c !== winner);
437
- warnings.push(
438
- `${url} has more than one index file: using ${winner.file}, ignoring ${losers.map((l) => l.file).join(", ")}.`
439
- );
440
- routes.push(winner);
441
- continue;
442
- }
443
- errors.push(
444
- `Duplicate route ${url} produced by ${candidates.length} files:
445
- ` + candidates.map((c) => ` - ${c.file}`).join("\n") + `
446
- Rename one of them, or exclude it with \`exclude\` in seemore.config.ts.`
447
- );
448
- }
449
- return { routes, errors, warnings };
450
- }
451
-
452
- // src/node/content/links.ts
453
883
  var CONTENT_EXT2 = /\.mdx?$/i;
454
884
  function createLinkResolver(pages, base) {
455
885
  const byPath = /* @__PURE__ */ new Map();
@@ -543,9 +973,9 @@ import { dynamicLoader } from "fumadocs-core/source";
543
973
  import { lucideIconsPlugin } from "fumadocs-core/source/plugins/lucide-icons";
544
974
 
545
975
  // src/node/content/scan.ts
546
- import { readFileSync } from "fs";
547
- import { createHash } from "crypto";
548
- import { basename, dirname as dirname2, join, resolve as resolve2 } from "path";
976
+ import { readFileSync as readFileSync4 } from "fs";
977
+ import { createHash as createHash2 } from "crypto";
978
+ import { basename, dirname as dirname5, join as join8, resolve as resolve3 } from "path";
549
979
  import { globSync } from "tinyglobby";
550
980
  import { z as z4 } from "zod";
551
981
 
@@ -615,7 +1045,7 @@ var metaSchema = z4.object({
615
1045
  description: z4.string().optional()
616
1046
  }).loose();
617
1047
  function scan(options) {
618
- const contentRoot = resolve2(options.contentRoot);
1048
+ const contentRoot = resolve3(options.contentRoot);
619
1049
  const ignore = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
620
1050
  const contentFiles = globSync(["**/*.md", "**/*.mdx"], {
621
1051
  cwd: contentRoot,
@@ -627,13 +1057,13 @@ function scan(options) {
627
1057
  const { routes, errors, warnings } = resolveRoutes(contentFiles);
628
1058
  const pages = [];
629
1059
  for (const route of routes) {
630
- const absPath = join(contentRoot, route.file);
1060
+ const absPath = join8(contentRoot, route.file);
631
1061
  let data;
632
1062
  let version;
633
1063
  try {
634
- const text = readFileSync(absPath, "utf8");
1064
+ const text = readFileSync4(absPath, "utf8");
635
1065
  data = parseFrontmatter(text, route.file).data;
636
- version = createHash("sha256").update(text).digest("hex").slice(0, 12);
1066
+ version = createHash2("sha256").update(text).digest("hex").slice(0, 12);
637
1067
  } catch (error) {
638
1068
  errors.push(error instanceof Error ? error.message : String(error));
639
1069
  continue;
@@ -651,9 +1081,9 @@ function scan(options) {
651
1081
  }));
652
1082
  const metaDirs = /* @__PURE__ */ new Set();
653
1083
  for (const file of metaFiles) {
654
- const absPath = join(contentRoot, file);
1084
+ const absPath = join8(contentRoot, file);
655
1085
  try {
656
- const parsed = metaSchema.safeParse(JSON.parse(readFileSync(absPath, "utf8")));
1086
+ const parsed = metaSchema.safeParse(JSON.parse(readFileSync4(absPath, "utf8")));
657
1087
  if (!parsed.success) {
658
1088
  errors.push(
659
1089
  `Invalid ${file}:
@@ -661,7 +1091,7 @@ ${parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.messa
661
1091
  );
662
1092
  continue;
663
1093
  }
664
- metaDirs.add(dirname2(file));
1094
+ metaDirs.add(dirname5(file));
665
1095
  files.push({ type: "meta", path: file, absolutePath: absPath, data: parsed.data });
666
1096
  } catch (error) {
667
1097
  errors.push(`Invalid ${file}: ${error instanceof Error ? error.message : String(error)}`);
@@ -673,7 +1103,7 @@ ${parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.messa
673
1103
  function synthesiseOrderMeta(pages, metaDirs) {
674
1104
  const byDir = /* @__PURE__ */ new Map();
675
1105
  for (const page of pages) {
676
- const dir = dirname2(page.file);
1106
+ const dir = dirname5(page.file);
677
1107
  const bucket = byDir.get(dir);
678
1108
  if (bucket) bucket.push(page);
679
1109
  else byDir.set(dir, [page]);
@@ -795,43 +1225,16 @@ function createContext(options) {
795
1225
  // src/cli/build.ts
796
1226
  init_paths();
797
1227
 
798
- // src/node/prerender/emit.ts
799
- import { mkdirSync, writeFileSync } from "fs";
800
- import { dirname as dirname4, join as join3 } from "path";
801
- function outputPathFor(url) {
802
- const clean = url.replace(/^\/+|\/+$/g, "");
803
- return clean === "" ? "index.html" : join3(clean, "index.html");
804
- }
805
- function writeHtml(outDir, relativePath, html) {
806
- const target = join3(outDir, relativePath);
807
- mkdirSync(dirname4(target), { recursive: true });
808
- writeFileSync(target, html, "utf8");
809
- }
810
- function applyTemplate(template, { html, head }) {
811
- return template.replace("<!--seemore-head-->", () => head).replace("<!--seemore-app-->", () => html);
812
- }
813
-
814
- // src/node/prerender/deploy.ts
815
- import { writeFileSync as writeFileSync2 } from "fs";
816
- import { join as join4 } from "path";
817
- function writeDeployArtifacts(outDir, base, shell) {
818
- const prefix = base === "/" ? "" : base.replace(/\/+$/, "");
819
- writeFileSync2(join4(outDir, "_redirects"), `${prefix}/* ${prefix}/index.html 200
820
- `, "utf8");
821
- writeFileSync2(join4(outDir, "200.html"), shell, "utf8");
822
- writeFileSync2(join4(outDir, ".nojekyll"), "", "utf8");
823
- }
824
-
825
1228
  // src/node/prerender/render.ts
826
1229
  import { pathToFileURL } from "url";
827
- import { join as join6 } from "path";
828
- import { build } from "vite";
1230
+ import { join as join10 } from "path";
1231
+ import { build as build2 } from "vite";
829
1232
 
830
1233
  // src/node/vite/config.ts
831
1234
  init_paths();
832
1235
  import { realpathSync as realpathSync2 } from "fs";
833
- import { createRequire as createRequire2 } from "module";
834
- import { join as join5 } from "path";
1236
+ import { createRequire as createRequire3 } from "module";
1237
+ import { join as join9 } from "path";
835
1238
  import react from "@vitejs/plugin-react";
836
1239
  import tailwindcss from "@tailwindcss/vite";
837
1240
  import mdx from "@mdx-js/rollup";
@@ -853,7 +1256,7 @@ import {
853
1256
 
854
1257
  // src/node/vite/remark.ts
855
1258
  import { existsSync as existsSync3 } from "fs";
856
- import { dirname as dirname5, relative, resolve as resolve4 } from "path";
1259
+ import { dirname as dirname6, relative, resolve as resolve4 } from "path";
857
1260
  import { visit } from "unist-util-visit";
858
1261
  var WIKILINK = /\[\[([^\]\n]+)\]\]/g;
859
1262
  var ALERTS = {
@@ -956,7 +1359,7 @@ function remarkSeemoreD2() {
956
1359
  function remarkSeemoreAssets(options) {
957
1360
  return (tree, file) => {
958
1361
  if (typeof file.path !== "string" || file.path === "") return;
959
- const dir = dirname5(file.path);
1362
+ const dir = dirname6(file.path);
960
1363
  const from = virtualPath(options.contentRoot, file);
961
1364
  visit(tree, "image", (node, index, parent) => {
962
1365
  if (parent === void 0 || index === void 0) return;
@@ -1093,12 +1496,12 @@ function createRehypePlugins(options = {}) {
1093
1496
  }
1094
1497
 
1095
1498
  // src/node/vite/plugin.ts
1096
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1097
- import { createRequire } from "module";
1098
- import { dirname as dirname6 } from "path";
1499
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1500
+ import { createRequire as createRequire2 } from "module";
1501
+ import { dirname as dirname7 } from "path";
1099
1502
 
1100
1503
  // src/node/search/build.ts
1101
- import { readFileSync as readFileSync3 } from "fs";
1504
+ import { readFileSync as readFileSync5 } from "fs";
1102
1505
  import { gzipSync } from "zlib";
1103
1506
  import { createFromSource } from "fumadocs-core/search/server";
1104
1507
  import { structure } from "fumadocs-core/mdx-plugins";
@@ -1114,7 +1517,7 @@ async function buildSearchIndex(ctx) {
1114
1517
  const bodies = /* @__PURE__ */ new Map();
1115
1518
  for (const page of ctx.pages()) {
1116
1519
  try {
1117
- bodies.set(page.url, parseFrontmatter(readFileSync3(page.absPath, "utf8"), page.file).content);
1520
+ bodies.set(page.url, parseFrontmatter(readFileSync5(page.absPath, "utf8"), page.file).content);
1118
1521
  } catch {
1119
1522
  }
1120
1523
  }
@@ -1187,11 +1590,11 @@ var PAGE_PREFIX = "seemore-page:";
1187
1590
  var resolvedId = (id) => `\0${id}`;
1188
1591
  var IMPORTS_MARKER = /\/\* seemore:imports[\s\S]*?\*\//;
1189
1592
  var USER_CSS_MARKER = /\/\* seemore:user-css[\s\S]*?\*\//;
1190
- var require_ = createRequire(import.meta.url);
1593
+ var require_2 = createRequire2(import.meta.url);
1191
1594
  function styleImports(ctx) {
1192
1595
  const lines = [`@import 'fumadocs-ui/css/${ctx.config.theme}.css';`];
1193
1596
  try {
1194
- lines.push(`@source '${dirname6(require_.resolve("fumadocs-ui/package.json"))}/dist';`);
1597
+ lines.push(`@source '${dirname7(require_2.resolve("fumadocs-ui/package.json"))}/dist';`);
1195
1598
  } catch {
1196
1599
  }
1197
1600
  return lines.join("\n");
@@ -1322,7 +1725,7 @@ function handleSourceRead(ctx, req, res) {
1322
1725
  if (page === void 0) return send(res, 404, { error: "That file is not part of this site." });
1323
1726
  const start = Number(query.get("start"));
1324
1727
  const end = Number(query.get("end"));
1325
- const content = readFileSync4(page.absPath, "utf8");
1728
+ const content = readFileSync6(page.absPath, "utf8");
1326
1729
  if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > content.length) {
1327
1730
  return send(res, 400, { error: "The requested range is not inside this file." });
1328
1731
  }
@@ -1340,7 +1743,7 @@ async function handleSourceWrite(ctx, req, res) {
1340
1743
  if (typeof body.expected !== "string" || typeof body.text !== "string") {
1341
1744
  return send(res, 400, { error: "Both `expected` and `text` are required." });
1342
1745
  }
1343
- const content = readFileSync4(page.absPath, "utf8");
1746
+ const content = readFileSync6(page.absPath, "utf8");
1344
1747
  const result = spliceSource(content, {
1345
1748
  start: body.start,
1346
1749
  end: body.end,
@@ -1348,7 +1751,7 @@ async function handleSourceWrite(ctx, req, res) {
1348
1751
  text: body.text
1349
1752
  });
1350
1753
  if (!result.ok) return send(res, result.status, { error: result.error });
1351
- writeFileSync3(page.absPath, result.content, "utf8");
1754
+ writeFileSync5(page.absPath, result.content, "utf8");
1352
1755
  return send(res, 200, { ok: true });
1353
1756
  }
1354
1757
  function resolvePage(ctx, file) {
@@ -1431,7 +1834,7 @@ function json(value) {
1431
1834
  }
1432
1835
  function readIfExists(path) {
1433
1836
  try {
1434
- return readFileSync4(path, "utf8");
1837
+ return readFileSync6(path, "utf8");
1435
1838
  } catch {
1436
1839
  return void 0;
1437
1840
  }
@@ -1514,12 +1917,17 @@ async function reloadById(server, id) {
1514
1917
  }
1515
1918
 
1516
1919
  // src/node/vite/config.ts
1517
- var require_2 = createRequire2(import.meta.url);
1920
+ var require_3 = createRequire3(import.meta.url);
1518
1921
  function d2BrowserEntry() {
1519
- const entry = require_2.resolve("@terrastruct/d2");
1520
- return join5(packageDirOf("@terrastruct/d2", entry), "dist", "browser", "index.js");
1922
+ const entry = require_3.resolve("@terrastruct/d2");
1923
+ return join9(packageDirOf("@terrastruct/d2", entry), "dist", "browser", "index.js");
1521
1924
  }
1522
- function createViteConfig({ ctx, mode, outDir, ssrOutDir }) {
1925
+ var HASHED_NAMES = {
1926
+ entryFileNames: "assets/[hash].js",
1927
+ chunkFileNames: "assets/[hash].js",
1928
+ assetFileNames: "assets/[hash][extname]"
1929
+ };
1930
+ function createViteConfig({ ctx, mode, outDir, ssrOutDir, auth = false }) {
1523
1931
  const root = appRoot();
1524
1932
  const isSsr = ssrOutDir !== void 0;
1525
1933
  const mdxOptions = {
@@ -1557,11 +1965,17 @@ function createViteConfig({ ctx, mode, outDir, ssrOutDir }) {
1557
1965
  tailwindcss(),
1558
1966
  ...mode === "dev" ? [seemoreWatcherPlugin(ctx)] : []
1559
1967
  ],
1968
+ // A compile-time constant rather than a field of `virtual:seemore/config`, which carries
1969
+ // nothing about protection. Defined either way, so an unprotected bundle drops the auth code.
1970
+ define: { "import.meta.env.SEEMORE_AUTH": JSON.stringify(auth) },
1560
1971
  // Vite bundles workers with the browser export condition, but a worker has no `document`.
1561
1972
  // `decode-named-character-reference` — pulled in through fumadocs' search client, via
1562
1973
  // remark — calls `document.createElement` at module scope in its browser build, so the
1563
1974
  // search worker threw on load. The package ships a DOM-free `worker` entry; use it.
1564
- worker: { plugins: () => [workerConditionPlugin()] },
1975
+ worker: {
1976
+ plugins: () => [workerConditionPlugin()],
1977
+ rollupOptions: auth ? { output: HASHED_NAMES } : void 0
1978
+ },
1565
1979
  resolve: {
1566
1980
  // The app is compiled from seemore's own sources, so its dependencies must resolve
1567
1981
  // from seemore's directory rather than from the user's project.
@@ -1596,7 +2010,7 @@ function createViteConfig({ ctx, mode, outDir, ssrOutDir }) {
1596
2010
  }
1597
2011
  },
1598
2012
  build: isSsr ? {
1599
- ssr: join5(root, "entry.prerender.tsx"),
2013
+ ssr: join9(root, "entry.prerender.tsx"),
1600
2014
  outDir: ssrOutDir,
1601
2015
  emptyOutDir: true,
1602
2016
  copyPublicDir: false,
@@ -1605,7 +2019,7 @@ function createViteConfig({ ctx, mode, outDir, ssrOutDir }) {
1605
2019
  } : {
1606
2020
  outDir,
1607
2021
  emptyOutDir: true,
1608
- rollupOptions: { input: join5(root, "index.html") },
2022
+ rollupOptions: { input: join9(root, "index.html"), output: auth ? HASHED_NAMES : void 0 },
1609
2023
  // The app bundle is seemore's own, not the user's; warning them about a size they
1610
2024
  // cannot act on is noise.
1611
2025
  chunkSizeWarningLimit: 2e3
@@ -1656,8 +2070,8 @@ var WORKER_SAFE_ENTRIES = /* @__PURE__ */ new Set(["decode-named-character-refer
1656
2070
 
1657
2071
  // src/node/prerender/render.ts
1658
2072
  async function loadPrerenderModule(ctx, ssrOutDir) {
1659
- await build(createViteConfig({ ctx, mode: "build", ssrOutDir }));
1660
- const entry = join6(ssrOutDir, "entry.prerender.js");
2073
+ await build2(createViteConfig({ ctx, mode: "build", ssrOutDir }));
2074
+ const entry = join10(ssrOutDir, "entry.prerender.js");
1661
2075
  const loaded = await import(pathToFileURL(entry).href);
1662
2076
  if (typeof loaded.render !== "function" || typeof loaded.renderArticle !== "function" || typeof loaded.listRoutes !== "function") {
1663
2077
  throw new Error(`seemore: the prerender build at ${entry} did not export \`render\`, \`renderArticle\` and \`listRoutes\`.`);
@@ -1666,8 +2080,8 @@ async function loadPrerenderModule(ctx, ssrOutDir) {
1666
2080
  }
1667
2081
 
1668
2082
  // src/node/social/cards.ts
1669
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
1670
- import { dirname as dirname7, join as join7 } from "path";
2083
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync6 } from "fs";
2084
+ import { dirname as dirname8, join as join11 } from "path";
1671
2085
 
1672
2086
  // src/shared/og.ts
1673
2087
  function ogImagePath(url) {
@@ -1690,9 +2104,9 @@ async function generateSocialCards(ctx, outDir) {
1690
2104
  for (const page of ctx.pages()) {
1691
2105
  const png = await renderCard(takumi, ctx.config.title, page.data.title, page.data.description);
1692
2106
  if (png === void 0) continue;
1693
- const target = join7(outDir, ogImagePath(page.url));
1694
- mkdirSync2(dirname7(target), { recursive: true });
1695
- writeFileSync4(target, png);
2107
+ const target = join11(outDir, ogImagePath(page.url));
2108
+ mkdirSync2(dirname8(target), { recursive: true });
2109
+ writeFileSync6(target, png);
1696
2110
  written++;
1697
2111
  }
1698
2112
  return written;
@@ -1734,6 +2148,7 @@ async function runBuild(options) {
1734
2148
  const ctx = createContext({ config, contentRoot });
1735
2149
  const outDir = resolve5(options.cwd, options.outDir ?? "dist");
1736
2150
  assertSafeOutDir(outDir, options.cwd, contentRoot);
2151
+ const password = config.auth === void 0 ? void 0 : readPassword("npx seemore build");
1737
2152
  const scan2 = ctx.source.current();
1738
2153
  failOnErrors(ctx.errors(), contentRoot);
1739
2154
  if (scan2.pages.length === 0) {
@@ -1741,9 +2156,29 @@ async function runBuild(options) {
1741
2156
  }
1742
2157
  for (const warning of scan2.warnings) ctx.warnings.add(warning);
1743
2158
  console.log(pc2.dim(`seemore ${scan2.pages.length} pages from ${relative2(options.cwd, contentRoot) || "."}`));
1744
- await viteBuild(createViteConfig({ ctx, mode: "build", outDir }));
1745
- const template = readFileSync5(join8(outDir, "index.html"), "utf8");
1746
- const ssrOutDir = mkdtempSync(join8(tmpdir2(), "seemore-ssr-"));
2159
+ await viteBuild(createViteConfig({ ctx, mode: "build", outDir, auth: password !== void 0 }));
2160
+ const template = readFileSync7(join12(outDir, "index.html"), "utf8");
2161
+ const routes = password === void 0 ? await prerenderPages(ctx, outDir, template) : countRoutes(ctx);
2162
+ if (config.search.provider === "static") {
2163
+ const index = await buildSearchIndex(ctx);
2164
+ mkdirSync3(join12(outDir, "api"), { recursive: true });
2165
+ writeFileSync7(join12(outDir, "api", "search.json"), index, "utf8");
2166
+ const size = measureIndex(index);
2167
+ console.log(pc2.dim(`seemore search index ${formatBytes(size.gzipped)} gzipped`));
2168
+ if (size.warning !== void 0) ctx.warnings.add(size.warning);
2169
+ }
2170
+ if (config.search.provider !== "static") await warnIfSearchSdkMissing(ctx, config.search.provider);
2171
+ if (config.features["social.cards"]) await generateSocialCards(ctx, outDir);
2172
+ if (password !== void 0) {
2173
+ const encrypted = await sealSite(ctx, outDir, template, password);
2174
+ console.log(pc2.dim(`seemore ${encrypted} files encrypted; visitors unlock them with the password`));
2175
+ }
2176
+ ctx.warnings.flush();
2177
+ console.log(pc2.green(`seemore ${routes} pages written to ${relative2(options.cwd, outDir) || outDir}`));
2178
+ return { outDir, routes };
2179
+ }
2180
+ async function prerenderPages(ctx, outDir, template) {
2181
+ const ssrOutDir = mkdtempSync(join12(tmpdir3(), "seemore-ssr-"));
1747
2182
  try {
1748
2183
  const prerender = await loadPrerenderModule(ctx, ssrOutDir);
1749
2184
  const urls = prerender.listRoutes();
@@ -1756,24 +2191,16 @@ async function runBuild(options) {
1756
2191
  }
1757
2192
  const notFound = applyTemplate(template, await prerender.render("/__seemore_not_found"));
1758
2193
  writeHtml(outDir, "404.html", notFound);
1759
- writeDeployArtifacts(outDir, config.base, notFound);
1760
- if (config.search.provider === "static") {
1761
- const index = await buildSearchIndex(ctx);
1762
- mkdirSync3(join8(outDir, "api"), { recursive: true });
1763
- writeFileSync5(join8(outDir, "api", "search.json"), index, "utf8");
1764
- const size = measureIndex(index);
1765
- console.log(pc2.dim(`seemore search index ${formatBytes(size.gzipped)} gzipped`));
1766
- if (size.warning !== void 0) ctx.warnings.add(size.warning);
1767
- }
1768
- if (config.search.provider !== "static") await warnIfSearchSdkMissing(ctx, config.search.provider);
1769
- if (config.features["social.cards"]) await generateSocialCards(ctx, outDir);
1770
- ctx.warnings.flush();
1771
- console.log(pc2.green(`seemore ${routes.length} pages written to ${relative2(options.cwd, outDir) || outDir}`));
1772
- return { outDir, routes: routes.length };
2194
+ writeDeployArtifacts(outDir, ctx.config.base, notFound);
2195
+ return routes.length;
1773
2196
  } finally {
1774
2197
  rmSync(ssrOutDir, { recursive: true, force: true });
1775
2198
  }
1776
2199
  }
2200
+ function countRoutes(ctx) {
2201
+ const pages = ctx.pages();
2202
+ return pages.some((page) => page.url === "/") ? pages.length : pages.length + 1;
2203
+ }
1777
2204
  function assertSafeOutDir(outDir, cwd, contentRoot) {
1778
2205
  const contains = (parent, child) => {
1779
2206
  const rel = relative2(parent, child);
@@ -1793,7 +2220,7 @@ function assertSafeOutDir(outDir, cwd, contentRoot) {
1793
2220
  async function warnIfSearchSdkMissing(ctx, provider) {
1794
2221
  const packageName = provider === "algolia" ? "algoliasearch" : "@orama/core";
1795
2222
  try {
1796
- createRequire3(join8(ctx.config.root, "noop.js")).resolve(packageName);
2223
+ createRequire4(join12(ctx.config.root, "noop.js")).resolve(packageName);
1797
2224
  } catch {
1798
2225
  ctx.warnings.add(
1799
2226
  `\`search.provider\` is '${provider}', which needs ${packageName}. Run \`npm install ${packageName}\` or search will find nothing.`
@@ -1839,6 +2266,9 @@ async function runDev(options) {
1839
2266
  if (scan2.pages.length === 0) {
1840
2267
  ctx.warnings.add(`No Markdown files found under ${contentRoot}. seemore will serve an empty site until there are.`);
1841
2268
  }
2269
+ if (config.auth !== void 0 && options.json !== true) {
2270
+ console.log(pc3.dim("seemore auth is build-only; the dev server is not password-protected"));
2271
+ }
1842
2272
  const base = createViteConfig({ ctx, mode: "dev" });
1843
2273
  const server = await createServer({
1844
2274
  ...base,
@@ -1873,9 +2303,9 @@ async function runDev(options) {
1873
2303
  }
1874
2304
 
1875
2305
  // src/cli/export.ts
1876
- import { existsSync as existsSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, readdirSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
1877
- import { basename as basename2, dirname as dirname8, extname, join as join9, relative as relative3, resolve as resolve6 } from "path";
1878
- import { tmpdir as tmpdir3 } from "os";
2306
+ import { existsSync as existsSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync8, readdirSync as readdirSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync8, mkdirSync as mkdirSync4 } from "fs";
2307
+ import { basename as basename2, dirname as dirname9, extname, join as join13, relative as relative3, resolve as resolve6 } from "path";
2308
+ import { tmpdir as tmpdir4 } from "os";
1879
2309
  import pc4 from "picocolors";
1880
2310
  import { build as viteBuild2 } from "vite";
1881
2311
  init_paths();
@@ -1913,7 +2343,7 @@ async function runExport(options) {
1913
2343
  if (!existsSync4(target) || !statSync(target).isFile()) {
1914
2344
  throw new Error(`No such file: ${options.file}`);
1915
2345
  }
1916
- const contentRoot = resolveContentRoot(options.cwd, dirname8(target));
2346
+ const contentRoot = resolveContentRoot(options.cwd, dirname9(target));
1917
2347
  const loaded = await loadConfig({ root: contentRoot, configPath: resolveConfigPath(options) });
1918
2348
  const config = {
1919
2349
  ...loaded.config,
@@ -1935,23 +2365,23 @@ ${errors.join("\n\n")}`);
1935
2365
  if (page === void 0) {
1936
2366
  throw new Error(`${options.file} is not part of this site \u2014 excluded in the config, or outside ${relative3(options.cwd, contentRoot) || "."}.`);
1937
2367
  }
1938
- const outDir = mkdtempSync2(join9(tmpdir3(), "seemore-export-"));
1939
- const ssrOutDir = mkdtempSync2(join9(tmpdir3(), "seemore-export-ssr-"));
2368
+ const outDir = mkdtempSync2(join13(tmpdir4(), "seemore-export-"));
2369
+ const ssrOutDir = mkdtempSync2(join13(tmpdir4(), "seemore-export-ssr-"));
1940
2370
  try {
1941
2371
  await viteBuild2(createViteConfig({ ctx, mode: "build", outDir }));
1942
2372
  const css = readBuiltCss(outDir);
1943
- const template = readFileSync6(join9(outDir, "index.html"), "utf8");
2373
+ const template = readFileSync8(join13(outDir, "index.html"), "utf8");
1944
2374
  const prerender = await loadPrerenderModule(ctx, ssrOutDir);
1945
2375
  const article = await prerender.renderArticle(page.url);
1946
2376
  const runtime = await bundleRuntime();
1947
2377
  const html = assemble({ article, css, runtime, config, outDir, contentRoot, template });
1948
2378
  const filename = `${basename2(target).replace(/\.(?:md|mdx)$/i, "")}.html`;
1949
- const targetPath = options.out === void 0 ? join9(dirname8(target), filename) : join9(resolve6(options.cwd, options.out), filename);
2379
+ const targetPath = options.out === void 0 ? join13(dirname9(target), filename) : join13(resolve6(options.cwd, options.out), filename);
1950
2380
  if (existsSync4(targetPath)) {
1951
2381
  console.log(pc4.yellow(`seemore replacing existing ${relative3(options.cwd, targetPath) || targetPath}`));
1952
2382
  }
1953
- mkdirSync4(dirname8(targetPath), { recursive: true });
1954
- writeFileSync6(targetPath, html, "utf8");
2383
+ mkdirSync4(dirname9(targetPath), { recursive: true });
2384
+ writeFileSync8(targetPath, html, "utf8");
1955
2385
  ctx.warnings.flush();
1956
2386
  console.log(pc4.green(`seemore wrote ${relative3(options.cwd, targetPath) || targetPath} (${formatBytes2(html.length)})`));
1957
2387
  } finally {
@@ -1960,13 +2390,13 @@ ${errors.join("\n\n")}`);
1960
2390
  }
1961
2391
  }
1962
2392
  function readBuiltCss(outDir) {
1963
- const assets = join9(outDir, "assets");
1964
- const files = existsSync4(assets) ? readdirSync(assets).filter((file) => file.endsWith(".css")) : [];
2393
+ const assets = join13(outDir, "assets");
2394
+ const files = existsSync4(assets) ? readdirSync2(assets).filter((file) => file.endsWith(".css")) : [];
1965
2395
  if (files.length === 0) throw new Error("The build produced no stylesheet \u2014 the export would be unstyled.");
1966
- return files.map((file) => readFileSync6(join9(assets, file), "utf8")).join("\n");
2396
+ return files.map((file) => readFileSync8(join13(assets, file), "utf8")).join("\n");
1967
2397
  }
1968
2398
  async function bundleRuntime() {
1969
- const dir = mkdtempSync2(join9(tmpdir3(), "seemore-export-runtime-"));
2399
+ const dir = mkdtempSync2(join13(tmpdir4(), "seemore-export-runtime-"));
1970
2400
  try {
1971
2401
  await viteBuild2({
1972
2402
  configFile: false,
@@ -1977,14 +2407,14 @@ async function bundleRuntime() {
1977
2407
  minify: true,
1978
2408
  target: "es2018",
1979
2409
  lib: {
1980
- entry: join9(appRoot(), "export", "standalone.ts"),
2410
+ entry: join13(appRoot(), "export", "standalone.ts"),
1981
2411
  name: "seemoreExport",
1982
2412
  formats: ["iife"],
1983
2413
  fileName: () => "runtime.js"
1984
2414
  }
1985
2415
  }
1986
2416
  });
1987
- return readFileSync6(join9(dir, "runtime.js"), "utf8");
2417
+ return readFileSync8(join13(dir, "runtime.js"), "utf8");
1988
2418
  } finally {
1989
2419
  rmSync2(dir, { recursive: true, force: true });
1990
2420
  }
@@ -2001,8 +2431,8 @@ function assemble(input) {
2001
2431
  "<head>",
2002
2432
  '<meta charset="utf-8">',
2003
2433
  '<meta name="viewport" content="width=device-width, initial-scale=1">',
2004
- `<title>${escapeHtml(title)}</title>`,
2005
- description === void 0 ? "" : `<meta name="description" content="${escapeHtml(description)}">`,
2434
+ `<title>${escapeHtml2(title)}</title>`,
2435
+ description === void 0 ? "" : `<meta name="description" content="${escapeHtml2(description)}">`,
2006
2436
  '<meta name="generator" content="seemore">',
2007
2437
  THEME_INIT,
2008
2438
  favicon,
@@ -2051,7 +2481,7 @@ function resolveAsset(src, base, outDir, contentRoot) {
2051
2481
  if (base !== "/" && path.startsWith(base)) path = path.slice(base.length - 1);
2052
2482
  const suffix = path.replace(/^\/+/, "");
2053
2483
  for (const root of [outDir, contentRoot]) {
2054
- const candidate = join9(root, suffix);
2484
+ const candidate = join13(root, suffix);
2055
2485
  if (existsSync4(candidate) && statSync(candidate).isFile()) return candidate;
2056
2486
  }
2057
2487
  return void 0;
@@ -2075,11 +2505,11 @@ var MIME = {
2075
2505
  };
2076
2506
  function dataUriFor(file) {
2077
2507
  const mime = MIME[extname(file).toLowerCase()] ?? "application/octet-stream";
2078
- return `data:${mime};base64,${readFileSync6(file).toString("base64")}`;
2508
+ return `data:${mime};base64,${readFileSync8(file).toString("base64")}`;
2079
2509
  }
2080
2510
  function faviconLink(config, contentRoot, template) {
2081
2511
  if (config.favicon !== void 0) {
2082
- const file = join9(contentRoot, config.favicon);
2512
+ const file = join13(contentRoot, config.favicon);
2083
2513
  if (existsSync4(file)) {
2084
2514
  try {
2085
2515
  return `<link rel="icon" href="${dataUriFor(file)}" />`;
@@ -2108,9 +2538,9 @@ function withExportToc(html) {
2108
2538
  }
2109
2539
  }
2110
2540
  const items = sections.map((section) => {
2111
- const self = `<a href="#${section.id}">${escapeHtml(section.text)}</a>`;
2541
+ const self = `<a href="#${section.id}">${escapeHtml2(section.text)}</a>`;
2112
2542
  if (section.children.length === 0) return `<li>${self}</li>`;
2113
- const kids = section.children.map((child) => `<li><a href="#${child.id}">${escapeHtml(child.text)}</a></li>`).join("");
2543
+ const kids = section.children.map((child) => `<li><a href="#${child.id}">${escapeHtml2(child.text)}</a></li>`).join("");
2114
2544
  return `<li>${self}<ul>${kids}</ul></li>`;
2115
2545
  }).join("");
2116
2546
  const toc = `<nav class="seemore-export-toc"><details><summary>On this page</summary><ul>${items}</ul></details></nav>`;
@@ -2120,7 +2550,7 @@ function withExportToc(html) {
2120
2550
  function decodeEntities(text) {
2121
2551
  return text.replaceAll("&amp;", "&").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", '"').replaceAll("&#39;", "'");
2122
2552
  }
2123
- function escapeHtml(value) {
2553
+ function escapeHtml2(value) {
2124
2554
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
2125
2555
  }
2126
2556
  function formatBytes2(size) {
@@ -2177,10 +2607,10 @@ async function main(argv = process.argv.slice(2)) {
2177
2607
  return;
2178
2608
  }
2179
2609
  if (values.version === true) {
2180
- const { readFileSync: readFileSync7 } = await import("fs");
2181
- const { join: join10 } = await import("path");
2610
+ const { readFileSync: readFileSync9 } = await import("fs");
2611
+ const { join: join14 } = await import("path");
2182
2612
  const { packageRoot: packageRoot2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
2183
- const pkg = JSON.parse(readFileSync7(join10(packageRoot2(), "package.json"), "utf8"));
2613
+ const pkg = JSON.parse(readFileSync9(join14(packageRoot2(), "package.json"), "utf8"));
2184
2614
  console.log(pkg.version);
2185
2615
  return;
2186
2616
  }