@naru-pub/dev 0.0.0-stage → 1.1.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.
@@ -0,0 +1,1230 @@
1
+ // src/naru.ts
2
+ import { spawn } from "node:child_process";
3
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
4
+ import { existsSync, watch } from "node:fs";
5
+ import {
6
+ mkdir,
7
+ mkdtemp,
8
+ readFile,
9
+ readdir,
10
+ rename,
11
+ rm,
12
+ stat,
13
+ writeFile
14
+ } from "node:fs/promises";
15
+ import {
16
+ createServer
17
+ } from "node:http";
18
+ import { connect, createServer as createNetServer } from "node:net";
19
+ import { tmpdir } from "node:os";
20
+ import { dirname, extname, join, relative, resolve, sep } from "node:path";
21
+
22
+ // ../control-plane/src/lib/site-data/validation.ts
23
+ var NAME = /^[a-zA-Z0-9_-]{1,64}$/;
24
+ var MAX_DOCUMENT_BYTES = 64 * 1024;
25
+ var MAX_SITE_BYTES = 100 * 1024 * 1024;
26
+ var DataError = class extends Error {
27
+ constructor(status, message, code, details, site) {
28
+ super(message);
29
+ this.status = status;
30
+ this.code = code;
31
+ this.details = details;
32
+ this.site = site;
33
+ }
34
+ };
35
+ function name(value) {
36
+ if (typeof value !== "string" || !NAME.test(value)) {
37
+ throw new DataError(
38
+ 400,
39
+ "Names must contain 1\u201364 letters, numbers, underscores or hyphens."
40
+ );
41
+ }
42
+ return value;
43
+ }
44
+ function functionModule(value) {
45
+ if (typeof value !== "string")
46
+ throw new DataError(400, "Invalid function module.");
47
+ return value.split("/").map(name).join("/");
48
+ }
49
+ function functionReference(value) {
50
+ const parts = value.split(":");
51
+ if (parts.length !== 2) throw new DataError(404, "Not found.");
52
+ return { file: functionModule(parts[0]), fn: name(parts[1]) };
53
+ }
54
+
55
+ // ../control-plane/src/lib/site-data/filters.ts
56
+ var MAX_FILTER_BYTES = 2048;
57
+ function parseFilterQuery(raw) {
58
+ if (raw === null) return void 0;
59
+ if (Buffer.byteLength(raw) > MAX_FILTER_BYTES)
60
+ throw new DataError(400, "Filter exceeds 2048 bytes.");
61
+ try {
62
+ return JSON.parse(raw);
63
+ } catch {
64
+ throw new DataError(400, "filter must be a JSON object.");
65
+ }
66
+ }
67
+
68
+ // ../control-plane/src/lib/site-data/live-ticket.ts
69
+ var TICKET_MS = 6e4;
70
+ var base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
71
+ async function key(secret) {
72
+ return crypto.subtle.importKey(
73
+ "raw",
74
+ new TextEncoder().encode(`naru-live-ticket:${secret}`),
75
+ { name: "HMAC", hash: "SHA-256" },
76
+ false,
77
+ ["sign", "verify"]
78
+ );
79
+ }
80
+ async function signLiveTicket(secret, site, member, now = Date.now()) {
81
+ const payload = base64url(
82
+ new TextEncoder().encode(
83
+ JSON.stringify({ s: site, m: member, e: now + TICKET_MS })
84
+ )
85
+ );
86
+ const signature = new Uint8Array(
87
+ await crypto.subtle.sign(
88
+ "HMAC",
89
+ await key(secret),
90
+ new TextEncoder().encode(payload)
91
+ )
92
+ );
93
+ return `${payload}.${base64url(signature)}`;
94
+ }
95
+ var OWNER_TICKET = "~owner";
96
+
97
+ // ../control-plane/src/lib/site-data/protocol.ts
98
+ var OWNER_EXPIRES = "Naru-Owner-Expires";
99
+ var OWNER_EXPIRES_IN = "Naru-Owner-Expires-In";
100
+ var publicHeaders = {
101
+ "Access-Control-Allow-Origin": "*",
102
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
103
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
104
+ // A token renews as it is used, so the SDK is told the expiry it now has;
105
+ // a 429 says when its limit resets.
106
+ "Access-Control-Expose-Headers": `${OWNER_EXPIRES}, ${OWNER_EXPIRES_IN}, Retry-After`,
107
+ "Access-Control-Max-Age": "600"
108
+ };
109
+ function encodeRevision(version) {
110
+ if (!Number.isSafeInteger(version) || Number(version) < 1)
111
+ throw new Error("Invalid stored document version.");
112
+ return `r1.${Number(version).toString(36)}`;
113
+ }
114
+ function decodeRevision(value) {
115
+ if (value === null) return void 0;
116
+ const match = /^r1\.([0-9a-z]+)$/.exec(value);
117
+ const version = match ? Number.parseInt(match[1], 36) : NaN;
118
+ if (!Number.isSafeInteger(version) || version < 1)
119
+ throw new DataError(400, "Invalid revision.");
120
+ return version;
121
+ }
122
+ function websiteCondition(url) {
123
+ const revision = url.searchParams.get("ifRevision");
124
+ const absent = url.searchParams.get("ifAbsent");
125
+ if (revision !== null && absent !== null)
126
+ throw new DataError(400, "Use one write condition.");
127
+ return absent === "1" ? 0 : decodeRevision(revision);
128
+ }
129
+ function publicResult(value) {
130
+ if (Array.isArray(value)) return value.map(publicResult);
131
+ if (value instanceof Date) return value;
132
+ if (!value || typeof value !== "object") return value;
133
+ const record = value;
134
+ return Object.fromEntries(
135
+ Object.entries(record).flatMap(
136
+ ([key2, item]) => key2 === "version" ? [["revision", encodeRevision(item)]] : [[key2, key2 === "data" ? item : publicResult(item)]]
137
+ )
138
+ );
139
+ }
140
+ var CALLS = {
141
+ _query: "query",
142
+ _mutate: "mutation"
143
+ };
144
+
145
+ // ../control-plane/src/lib/site-functions/routes.ts
146
+ var NO_ROUTES = Object.freeze({
147
+ include: [],
148
+ exclude: []
149
+ });
150
+ var MAX_RULES = 100;
151
+ var MAX_RULE_LENGTH = 100;
152
+ var refused = (message) => new DataError(400, `_naru/routes.json: ${message}`);
153
+ function routeRules(text) {
154
+ let value;
155
+ try {
156
+ value = JSON.parse(text);
157
+ } catch {
158
+ throw refused("not JSON.");
159
+ }
160
+ if (!value || typeof value !== "object" || Array.isArray(value))
161
+ throw refused('an object of "version", "include" and "exclude".');
162
+ const {
163
+ version,
164
+ include,
165
+ exclude = [],
166
+ ...rest
167
+ } = value;
168
+ if (Object.keys(rest).length)
169
+ throw refused(
170
+ `unknown ${Object.keys(rest).map((key2) => `"${key2}"`).join(", ")}.`
171
+ );
172
+ if (version !== 1) throw refused('"version" is 1.');
173
+ const rules = (list, name2) => {
174
+ if (!Array.isArray(list)) throw refused(`"${name2}" is a list of paths.`);
175
+ for (const rule of list)
176
+ if (typeof rule !== "string" || !rule.startsWith("/") || rule.length > MAX_RULE_LENGTH)
177
+ throw refused(
178
+ `${JSON.stringify(rule)} in "${name2}" is not a path from / of at most ${MAX_RULE_LENGTH} characters.`
179
+ );
180
+ return list;
181
+ };
182
+ const routes = {
183
+ include: rules(include, "include"),
184
+ exclude: rules(exclude, "exclude")
185
+ };
186
+ if (!routes.include.length) throw refused('"include" needs a path.');
187
+ if (routes.include.length + routes.exclude.length > MAX_RULES)
188
+ throw refused(`at most ${MAX_RULES} rules.`);
189
+ return routes;
190
+ }
191
+ function takes(rule, pathname) {
192
+ const parts = rule.split("*");
193
+ if (parts.length === 1) return rule === pathname;
194
+ if (!pathname.startsWith(parts[0]) || !pathname.endsWith(parts.at(-1)))
195
+ return false;
196
+ let at = parts[0].length;
197
+ const end = pathname.length - parts.at(-1).length;
198
+ if (end < at) return false;
199
+ for (const part of parts.slice(1, -1)) {
200
+ const found = pathname.indexOf(part, at);
201
+ if (found < 0 || found + part.length > end) return false;
202
+ at = found + part.length;
203
+ }
204
+ return true;
205
+ }
206
+ function routed(rules, pathname) {
207
+ return !rules.exclude.some((rule) => takes(rule, pathname)) && rules.include.some((rule) => takes(rule, pathname));
208
+ }
209
+ var BODY_TYPES = [
210
+ "text/html",
211
+ "text/plain",
212
+ "text/css",
213
+ "text/calendar",
214
+ "text/xml",
215
+ "application/xml",
216
+ "application/rss+xml",
217
+ "application/atom+xml",
218
+ "application/json"
219
+ ];
220
+ var invalid = (message) => new DataError(500, `The route answered ${message}`);
221
+ function routeAnswer(value) {
222
+ if (typeof value === "string")
223
+ return { status: 200, cache: 0, body: value, type: "text/html" };
224
+ if (!value || typeof value !== "object" || Array.isArray(value))
225
+ throw invalid("neither HTML nor { body }, { file } or { redirect }.");
226
+ const answer = value;
227
+ const status = answer.status ?? (answer.redirect !== void 0 ? 302 : 200);
228
+ if (!Number.isInteger(status) || status < 200 || status > 599)
229
+ throw invalid("a status outside 200\u2013599.");
230
+ const cache = answer.cache ?? 0;
231
+ if (!Number.isInteger(cache) || cache < 0 || cache > 300)
232
+ throw invalid("a cache other than 0\u2013300 seconds.");
233
+ const base = { status, cache };
234
+ if (typeof answer.body === "string") {
235
+ if (typeof answer.type !== "string" || !BODY_TYPES.includes(answer.type))
236
+ throw invalid(
237
+ `a body whose type is not one of ${BODY_TYPES.join(", ")}.`
238
+ );
239
+ return { ...base, body: answer.body, type: answer.type };
240
+ }
241
+ if (typeof answer.file === "string") {
242
+ const file = answer.file.replace(/^\/+/, "");
243
+ if (!file || file.split("/").some((part) => part === ".." || part === "."))
244
+ throw invalid("a file outside the site.");
245
+ if (answer.head !== void 0 && typeof answer.head !== "string")
246
+ throw invalid("a head that is not a string.");
247
+ return { ...base, file, head: answer.head ?? "" };
248
+ }
249
+ if (typeof answer.redirect === "string") {
250
+ if (![301, 302, 303, 307, 308].includes(base.status))
251
+ throw invalid("a redirect without a redirect status.");
252
+ if (!/^(\/(?!\/)|https:\/\/)/.test(answer.redirect))
253
+ throw invalid("a redirect to neither a path nor an https URL.");
254
+ return { ...base, redirect: answer.redirect };
255
+ }
256
+ throw invalid("neither HTML nor { body }, { file } or { redirect }.");
257
+ }
258
+ function withHead(html, head) {
259
+ if (!head) return html;
260
+ const at = html.search(/<\/head\s*>/i);
261
+ return at < 0 ? head + html : html.slice(0, at) + head + html.slice(at);
262
+ }
263
+
264
+ // src/naru.ts
265
+ var OWNER = "1";
266
+ var SITE_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
267
+ var FOLDERS = ["modules", "migrations", "queries", "mutations"];
268
+ var ROUTE_FILES = ["routes.json", "routes.js"];
269
+ var MEDIA_ORIGIN = "https://media.naru.pub";
270
+ var TYPES = {
271
+ ".html": "text/html; charset=utf-8",
272
+ ".js": "text/javascript; charset=utf-8",
273
+ ".mjs": "text/javascript; charset=utf-8",
274
+ ".css": "text/css; charset=utf-8",
275
+ ".json": "application/json",
276
+ ".svg": "image/svg+xml",
277
+ ".png": "image/png",
278
+ ".jpg": "image/jpeg",
279
+ ".jpeg": "image/jpeg",
280
+ ".gif": "image/gif",
281
+ ".webp": "image/webp",
282
+ ".ico": "image/x-icon",
283
+ ".txt": "text/plain; charset=utf-8",
284
+ ".xml": "application/xml",
285
+ ".wasm": "application/wasm",
286
+ ".woff2": "font/woff2",
287
+ ".ts": "text/plain; charset=utf-8"
288
+ };
289
+ var freePort = () => new Promise((done, fail) => {
290
+ const probe = createNetServer().listen(0, "127.0.0.1", () => {
291
+ const { port } = probe.address();
292
+ probe.close(() => done(port));
293
+ });
294
+ probe.on("error", fail);
295
+ });
296
+ async function readBody(req) {
297
+ const chunks = [];
298
+ for await (const chunk of req) chunks.push(chunk);
299
+ return Buffer.concat(chunks);
300
+ }
301
+ var json = (res, status, value) => res.writeHead(status, {
302
+ "content-type": "application/json",
303
+ "cache-control": "no-store"
304
+ }).end(JSON.stringify(value));
305
+ var escape = (text) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
306
+ var shortest = (path) => {
307
+ const near = relative(process.cwd(), path) || ".";
308
+ return near.length < path.length ? near : path;
309
+ };
310
+ async function files(dir) {
311
+ if (!existsSync(dir)) return [];
312
+ const found = [];
313
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
314
+ const path = join(dir, entry.name);
315
+ if (entry.isDirectory()) found.push(...await files(path));
316
+ else if (/\.(m?js|sql)$/.test(entry.name)) found.push(path);
317
+ }
318
+ return found;
319
+ }
320
+ async function start(options, resources) {
321
+ const folder = resolve(options.site);
322
+ const site = options.name ?? "localsite";
323
+ if (!SITE_NAME.test(site))
324
+ throw new TypeError(
325
+ "The site's name is a Naru login name: lowercase letters, digits, hyphens."
326
+ );
327
+ if (!existsSync(folder)) throw new Error(`There is no folder ${folder}.`);
328
+ const madeState = !options.state;
329
+ const state = options.state ? resolve(options.state) : await mkdtemp(join(tmpdir(), "naru-dev-"));
330
+ const mediaDir = join(state, "media", OWNER);
331
+ const bucketDir = join(state, "bucket");
332
+ await mkdir(mediaDir, { recursive: true });
333
+ let output = "";
334
+ const log = (line) => {
335
+ output += `${line}
336
+ `;
337
+ if (!options.quiet) console.log(line);
338
+ };
339
+ const warn = (line) => {
340
+ output += `${line}
341
+ `;
342
+ if (!options.quiet) console.error(line);
343
+ };
344
+ const secret = randomBytes(24).toString("base64url");
345
+ let origin = "";
346
+ let workerUrl = "";
347
+ let workerPort = 0;
348
+ let ready;
349
+ const started = new Promise((done) => ready = done);
350
+ const bucketPath = (key2) => {
351
+ const path = resolve(bucketDir, key2);
352
+ if (!path.startsWith(bucketDir + sep))
353
+ throw new DataError(400, "Invalid key.");
354
+ return path;
355
+ };
356
+ async function bucketKeys(dir) {
357
+ if (!existsSync(dir)) return [];
358
+ const found = [];
359
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
360
+ const path = join(dir, entry.name);
361
+ if (entry.isDirectory()) found.push(...await bucketKeys(path));
362
+ else if (!entry.name.endsWith(".naru-dev-partial")) found.push(path);
363
+ }
364
+ return found;
365
+ }
366
+ async function bucket(req, res, url) {
367
+ if (req.headers.authorization !== `Bearer ${secret}`)
368
+ return res.writeHead(401).end();
369
+ const rest = url.pathname.slice("/_naru-dev/bucket/".length);
370
+ if (rest === "list") {
371
+ const prefix = url.searchParams.get("prefix") ?? "";
372
+ const objects = [];
373
+ for (const path2 of (await bucketKeys(bucketDir)).sort()) {
374
+ const key3 = relative(bucketDir, path2).split(sep).join("/");
375
+ if (!key3.startsWith(prefix)) continue;
376
+ const info = await stat(path2);
377
+ objects.push({
378
+ key: key3,
379
+ size: info.size,
380
+ etag: `${info.size}-${info.mtimeMs}`,
381
+ uploaded: info.mtime.toISOString()
382
+ });
383
+ }
384
+ return json(res, 200, { objects, truncated: false });
385
+ }
386
+ if (!rest.startsWith("objects/")) return res.writeHead(404).end();
387
+ const key2 = rest.slice("objects/".length).split("/").map(decodeURIComponent).join("/");
388
+ const path = bucketPath(key2);
389
+ if (req.method === "GET") {
390
+ if (!existsSync(path)) return res.writeHead(404).end();
391
+ const content = await readFile(path);
392
+ const info = await stat(path);
393
+ return res.writeHead(200, {
394
+ etag: `${info.size}-${info.mtimeMs}`,
395
+ "content-length": content.length
396
+ }).end(content);
397
+ }
398
+ if (req.method === "PUT") {
399
+ const body = await readBody(req);
400
+ if (req.headers["if-none-match"] === "*" && existsSync(path))
401
+ return res.writeHead(412).end();
402
+ await mkdir(dirname(path), { recursive: true });
403
+ const partial = `${path}.${randomBytes(6).toString("hex")}.naru-dev-partial`;
404
+ try {
405
+ await writeFile(partial, body);
406
+ await rename(partial, path);
407
+ } catch (error) {
408
+ await rm(partial, { force: true });
409
+ throw error;
410
+ }
411
+ return res.writeHead(200).end();
412
+ }
413
+ if (req.method === "DELETE") {
414
+ await rm(path, { force: true });
415
+ return res.writeHead(204).end();
416
+ }
417
+ return res.writeHead(405).end();
418
+ }
419
+ async function object(operation, input) {
420
+ const response = await fetch(`${workerUrl}/v1/sites/${site}/${operation}`, {
421
+ method: "POST",
422
+ headers: {
423
+ authorization: `Bearer ${secret}`,
424
+ "content-type": "application/json"
425
+ },
426
+ body: JSON.stringify(input)
427
+ });
428
+ const answer = await response.json();
429
+ if (answer.error)
430
+ throw new DataError(
431
+ answer.error.status,
432
+ answer.error.message,
433
+ answer.error.code,
434
+ answer.error.details,
435
+ answer.error.site
436
+ );
437
+ return answer.value;
438
+ }
439
+ let worker = null;
440
+ let stopping = false;
441
+ async function startWorker() {
442
+ workerPort = await freePort();
443
+ workerUrl = `http://127.0.0.1:${workerPort}`;
444
+ const objects = join(state, "worker", "v3", "do");
445
+ await mkdir(objects, { recursive: true });
446
+ const child = spawn(
447
+ resources.workerd,
448
+ [
449
+ "serve",
450
+ join(resources.workerDir, "config.capnp"),
451
+ "--directory-path",
452
+ `site-objects=${objects}`,
453
+ "--directory-path",
454
+ `sdk-files=${resources.sdkDir}`,
455
+ "--socket-addr",
456
+ `http=127.0.0.1:${workerPort}`
457
+ ],
458
+ {
459
+ cwd: resources.workerDir,
460
+ env: {
461
+ ...process.env,
462
+ EDGE_WORKER_SECRET: secret,
463
+ PASSTHROUGH_ORIGIN: origin.replace("localhost", "127.0.0.1"),
464
+ SITE_BUCKET_URL: `${origin.replace("localhost", "127.0.0.1")}/_naru-dev/bucket/`
465
+ },
466
+ stdio: ["ignore", "pipe", "pipe"]
467
+ }
468
+ );
469
+ worker = child;
470
+ const orphaned = () => child.kill();
471
+ process.on("exit", orphaned);
472
+ child.once("exit", () => process.off("exit", orphaned));
473
+ const relay = (chunk) => {
474
+ for (const line of chunk.toString("utf8").split("\n"))
475
+ if (line.trim() && !/compatibility flag nodejs_compat became/.test(line))
476
+ warn(line);
477
+ };
478
+ child.stdout.on("data", relay);
479
+ child.stderr.on("data", relay);
480
+ let exited;
481
+ child.once("exit", (code) => {
482
+ exited = code;
483
+ if (!stopping) warn(`naru-dev: the edge Worker stopped (${code}).`);
484
+ });
485
+ for (let tries = 0; ; tries++) {
486
+ if (exited !== void 0)
487
+ throw new Error(`The edge Worker exited (${exited}):
488
+ ${output}`);
489
+ try {
490
+ if ((await fetch(`${workerUrl}/healthz`)).ok) break;
491
+ } catch {
492
+ }
493
+ if (tries > 300) throw new Error("The edge Worker did not start.");
494
+ await new Promise((done) => setTimeout(done, 100));
495
+ }
496
+ }
497
+ async function publish(path, publishing = true) {
498
+ const inside = relative(join(folder, "_naru"), path).split(sep);
499
+ const root = inside.length === 1 && ROUTE_FILES.includes(inside[0]);
500
+ const kind = root ? "root" : inside[0];
501
+ if (!root && (!FOLDERS.includes(kind) || !(kind === "migrations" ? /\.sql$/ : /\.m?js$/).test(path)))
502
+ return;
503
+ const file = root ? inside[0] : inside.slice(1).join("/").replace(/\.(m?js|sql)$/, "");
504
+ const source = existsSync(path) ? await readFile(path, "utf8") : null;
505
+ try {
506
+ await object("putFunctions", {
507
+ site,
508
+ folder: kind,
509
+ file,
510
+ source,
511
+ publish: publishing
512
+ });
513
+ if (publishing) {
514
+ log(`published _naru/ (${root ? file : `${kind}/${file}`} changed)`);
515
+ await writeTypes();
516
+ }
517
+ } catch (error) {
518
+ warn(
519
+ `_naru/${root ? file : `${kind}/${file}`}: ${error.message}`
520
+ );
521
+ }
522
+ }
523
+ const SETTLE_MS = 200;
524
+ const changed = /* @__PURE__ */ new Set();
525
+ const settled = /* @__PURE__ */ new Set();
526
+ let settling;
527
+ let draining = false;
528
+ function republish(path) {
529
+ changed.add(path);
530
+ clearTimeout(settling);
531
+ settling = setTimeout(() => {
532
+ for (const path2 of changed) settled.add(path2);
533
+ changed.clear();
534
+ void drain();
535
+ }, SETTLE_MS);
536
+ }
537
+ async function drain() {
538
+ if (draining) return;
539
+ draining = true;
540
+ try {
541
+ while (settled.size && !stopping) {
542
+ const [path] = settled;
543
+ settled.delete(path);
544
+ await publish(path);
545
+ }
546
+ } finally {
547
+ draining = false;
548
+ }
549
+ }
550
+ async function writeTypes() {
551
+ const tables = await object("siteTables", {});
552
+ const typeOf = (declared) => {
553
+ const type = declared.toUpperCase();
554
+ if (type.includes("INT")) return "number";
555
+ if (/CHAR|CLOB|TEXT/.test(type)) return "string";
556
+ if (type.includes("BLOB") || !type) return "unknown";
557
+ return "number";
558
+ };
559
+ const pascal = (name2) => name2.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join("").replace(/^(?=\d)/, "T");
560
+ const key2 = (name2) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name2) ? name2 : JSON.stringify(name2);
561
+ const body = [
562
+ "// Written by naru dev from the site's tables: do not edit.",
563
+ "",
564
+ ...tables.flatMap((table) => [
565
+ `/** ${table.view ? "View" : "Table"} ${table.name}. */`,
566
+ `export interface ${pascal(table.name)} {`,
567
+ ...table.columns.map(
568
+ (column) => ` ${key2(column.name)}: ${typeOf(column.type)}${column.nullable ? " | null" : ""};`
569
+ ),
570
+ "}",
571
+ ""
572
+ ]),
573
+ "export interface Database {",
574
+ ...tables.map((table) => ` ${key2(table.name)}: ${pascal(table.name)};`),
575
+ "}",
576
+ ""
577
+ ].join("\n");
578
+ const path = join(folder, "_naru", "types", "database.d.ts");
579
+ if (existsSync(path) && await readFile(path, "utf8") === body) return;
580
+ await mkdir(dirname(path), { recursive: true });
581
+ await writeFile(path, body);
582
+ log("wrote _naru/types/database.d.ts");
583
+ }
584
+ async function installDeclaration() {
585
+ const path = join(folder, "_naru", "collections.json");
586
+ if (!existsSync(path)) return;
587
+ try {
588
+ const declared = JSON.parse(await readFile(path, "utf8")).collections;
589
+ if (!declared || typeof declared !== "object")
590
+ throw new Error('it needs a "collections" object');
591
+ const names = Object.keys(declared);
592
+ for (const name2 of names)
593
+ if (!declared[name2]?.read || !declared[name2]?.write)
594
+ throw new Error(`"${name2}" needs both "read" and "write"`);
595
+ const existing = await object("collections", { names });
596
+ const wanted = names.map((name2) => ({
597
+ name: name2,
598
+ read_access: declared[name2].read,
599
+ write_access: declared[name2].write,
600
+ ...declared[name2].unique !== void 0 ? { unique: declared[name2].unique } : {},
601
+ ...declared[name2].indexes !== void 0 ? { indexes: declared[name2].indexes } : {}
602
+ }));
603
+ const differing = wanted.filter((want) => {
604
+ const row = existing.find((r) => r.name === want.name);
605
+ return !row || row.read_access !== want.read_access || row.write_access !== want.write_access || "unique" in want && (row.unique ?? []).join(",") !== (want.unique ?? []).join(",") || "indexes" in want && (row.indexes ?? []).join(",") !== (want.indexes ?? []).join(",");
606
+ });
607
+ if (!differing.length) return;
608
+ const created = await object("configureCollections", {
609
+ ownerId: OWNER,
610
+ collections: differing
611
+ });
612
+ log(
613
+ `_naru/collections.json: ${created?.length ?? 0} created, ${differing.length - (created?.length ?? 0)} changed`
614
+ );
615
+ } catch (error) {
616
+ warn(`_naru/collections.json: ${error.message}`);
617
+ }
618
+ }
619
+ async function install() {
620
+ await object("configure", {
621
+ ownerId: OWNER,
622
+ entitledUntil: null,
623
+ confirmedUntil: Date.now() + 10 * 365 * 86400 * 1e3,
624
+ monthlyRequests: null
625
+ });
626
+ const everything = [
627
+ ...(await Promise.all(
628
+ FOLDERS.map((kind) => files(join(folder, "_naru", kind)))
629
+ )).flat(),
630
+ ...ROUTE_FILES.map((name2) => join(folder, "_naru", name2)).filter(
631
+ (path) => existsSync(path)
632
+ )
633
+ ];
634
+ for (const [index, path] of everything.entries())
635
+ await publish(path, index === everything.length - 1);
636
+ await installDeclaration();
637
+ await recordUploads([...uploads.values()], true);
638
+ }
639
+ const pending = /* @__PURE__ */ new Map();
640
+ const tokensFile = join(state, "tokens.json");
641
+ const saved = existsSync(tokensFile) ? JSON.parse(await readFile(tokensFile, "utf8")) : { owners: [], members: [] };
642
+ const owners = new Set(saved.owners);
643
+ const memberTokens = new Map(saved.members);
644
+ const saveTokens = () => writeFile(
645
+ tokensFile,
646
+ JSON.stringify({ owners: [...owners], members: [...memberTokens] })
647
+ );
648
+ const membersFile = join(state, "members.json");
649
+ const members = async () => existsSync(membersFile) ? JSON.parse(await readFile(membersFile, "utf8")) : {};
650
+ const memberId = (name2) => {
651
+ const hex = createHash("sha256").update(`${site}:${name2}`).digest("hex");
652
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-7${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
653
+ };
654
+ async function keepMember(name2, status) {
655
+ const all = await members();
656
+ const id = memberId(name2);
657
+ all[id] = { name: name2, status: status ?? all[id]?.status ?? "approved" };
658
+ await writeFile(membersFile, JSON.stringify(all, null, 2));
659
+ return id;
660
+ }
661
+ const digest = (verifier) => createHash("sha256").update(verifier).digest("base64url");
662
+ const token = () => randomBytes(32).toString("base64url").slice(0, 43);
663
+ const LIFETIME = 7 * 86400;
664
+ async function issue(member) {
665
+ const issued = member ? `m.${token()}` : token();
666
+ if (member) memberTokens.set(issued, member);
667
+ else owners.add(issued);
668
+ await saveTokens();
669
+ return issued;
670
+ }
671
+ function redirectWithCode(res, query, member) {
672
+ const code = token();
673
+ pending.set(code, {
674
+ challenge: query.get("challenge"),
675
+ redirectUri: query.get("redirectUri"),
676
+ member
677
+ });
678
+ const back = new URL(query.get("redirectUri"));
679
+ back.searchParams.set("code", code);
680
+ back.searchParams.set("state", query.get("state"));
681
+ res.writeHead(302, { location: back.href }).end();
682
+ }
683
+ async function ownerCollections(names) {
684
+ const existing = await object("collections", {
685
+ names
686
+ });
687
+ const created = await object("configureCollections", {
688
+ ownerId: OWNER,
689
+ collections: names.filter((name2) => !existing.some((row) => row.name === name2)).map((name2) => ({
690
+ name: name2,
691
+ read_access: "admin",
692
+ write_access: "admin"
693
+ }))
694
+ });
695
+ log(
696
+ `owner signed in: ${names.length} collections, ${created?.length ?? 0} created`
697
+ );
698
+ }
699
+ async function ownerSignIn(res, query) {
700
+ await ownerCollections(
701
+ (query.get("collections") ?? "").split(",").filter(Boolean)
702
+ );
703
+ redirectWithCode(res, query);
704
+ }
705
+ async function memberSignIn(res, query) {
706
+ const name2 = query.getAll("as").find((value) => value.trim())?.trim();
707
+ if (name2)
708
+ return redirectWithCode(
709
+ res,
710
+ query,
711
+ await keepMember(name2, query.get("status"))
712
+ );
713
+ const keep = [...query].filter(([key2]) => !["as", "status"].includes(key2));
714
+ const hidden = keep.map(
715
+ ([key2, value]) => `<input type="hidden" name="${escape(key2)}" value="${escape(value)}">`
716
+ ).join("");
717
+ const known = Object.values(await members()).map(
718
+ (member) => `<button name="as" value="${escape(member.name)}">${escape(member.name)} (${escape(member.status)})</button>`
719
+ ).join(" ");
720
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(`<!doctype html><meta charset="utf-8"><title>naru-dev member sign-in</title>
721
+ <body style="font:14px sans-serif;max-width:560px;margin:40px auto">
722
+ <h1>${escape(site)} \uD68C\uC6D0\uC73C\uB85C \uB85C\uADF8\uC778 (naru-dev)</h1>
723
+ <form>${hidden}<p>${known || "\uC544\uC9C1 \uD14C\uC2A4\uD2B8 \uD68C\uC6D0\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."}</p>
724
+ <p><label>\uC0C8 \uD14C\uC2A4\uD2B8 \uD68C\uC6D0 \uC774\uB984 <input name="as"></label>
725
+ <select name="status"><option value="approved">approved</option><option value="pending">pending</option><option value="banned">banned</option></select>
726
+ <button>\uB85C\uADF8\uC778</button></p></form>`);
727
+ }
728
+ const uploads = /* @__PURE__ */ new Map();
729
+ const uploadsFile = join(state, "uploads.json");
730
+ if (existsSync(uploadsFile))
731
+ for (const upload of JSON.parse(
732
+ await readFile(uploadsFile, "utf8")
733
+ ))
734
+ uploads.set(upload.id, upload);
735
+ const saveUploads = () => writeFile(uploadsFile, JSON.stringify([...uploads.values()], null, 2));
736
+ const mediaUrl = (upload) => `${MEDIA_ORIGIN}/${OWNER}/${upload.file}`;
737
+ async function recordUploads(list, all = false) {
738
+ for (let at = 0; at < list.length; at += 1e3)
739
+ await object("recordMedia", {
740
+ files: list.slice(at, at + 1e3).map((upload) => ({
741
+ id: upload.id,
742
+ url: mediaUrl(upload),
743
+ member: upload.member,
744
+ name: upload.name,
745
+ type: upload.contentType,
746
+ size: upload.size
747
+ }))
748
+ });
749
+ if (all)
750
+ await object("recordMedia", {
751
+ files: [],
752
+ keep: list.map((upload) => upload.id)
753
+ });
754
+ }
755
+ const serviceWorker = `self.addEventListener("install", () => self.skipWaiting());
756
+ self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
757
+ self.addEventListener("fetch", (event) => {
758
+ const url = new URL(event.request.url);
759
+ if (url.origin === ${JSON.stringify(MEDIA_ORIGIN)})
760
+ event.respondWith(fetch("/media" + url.pathname));
761
+ });`;
762
+ async function executeMedia(method, path, body, who) {
763
+ if (method === "POST" && !path.length) {
764
+ const name2 = String(body?.name ?? "upload");
765
+ const id = randomUUID();
766
+ const extension = name2.includes(".") ? name2.split(".").pop().toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 10) : "";
767
+ uploads.set(id, {
768
+ id,
769
+ name: name2,
770
+ contentType: String(body?.contentType ?? "application/octet-stream"),
771
+ size: Number(body?.size ?? 0),
772
+ file: `${id}${extension ? `.${extension}` : ""}`,
773
+ member: who.member
774
+ });
775
+ return {
776
+ id,
777
+ uploadUrl: `${origin}/_naru-dev/upload/${id}`,
778
+ headers: { "Content-Type": String(body?.contentType) }
779
+ };
780
+ }
781
+ const upload = uploads.get(path[0]);
782
+ if (!upload || who.member && upload.member !== who.member)
783
+ throw new DataError(404, "File not found.");
784
+ if (method === "PUT") {
785
+ await saveUploads();
786
+ await recordUploads([upload]);
787
+ return {
788
+ file: {
789
+ id: upload.id,
790
+ name: upload.name,
791
+ contentType: upload.contentType,
792
+ size: upload.size,
793
+ url: mediaUrl(upload)
794
+ }
795
+ };
796
+ }
797
+ if (method === "DELETE") {
798
+ await rm(join(mediaDir, upload.file), { force: true });
799
+ uploads.delete(upload.id);
800
+ await saveUploads();
801
+ await object("forgetMedia", { ids: [upload.id] });
802
+ return { success: true };
803
+ }
804
+ throw new DataError(405, "Method not allowed.");
805
+ }
806
+ const failure = (res, error) => {
807
+ if (error instanceof DataError)
808
+ return json(res, error.status, {
809
+ error: {
810
+ code: error.code,
811
+ message: error.message,
812
+ ...error.details === void 0 ? {} : { details: error.details },
813
+ ...error.site ? { site: true } : {}
814
+ }
815
+ });
816
+ warn(String(error?.stack ?? error));
817
+ return json(res, 500, { error: { message: "naru-dev failed." } });
818
+ };
819
+ async function signed(req, res, url, path) {
820
+ const bearer = /^Bearer (.+)$/.exec(req.headers.authorization ?? "")?.[1];
821
+ const member = bearer ? memberTokens.get(bearer) ?? null : null;
822
+ const owner = bearer ? owners.has(bearer) : false;
823
+ if (bearer && !owner && !member)
824
+ throw new DataError(
825
+ 401,
826
+ "Sign-in is invalid or expired.",
827
+ "AUTH_REQUIRED"
828
+ );
829
+ if (member) {
830
+ const status = (await members())[member]?.status ?? "approved";
831
+ if (status !== "approved")
832
+ throw new DataError(403, `Membership is ${status}.`, "ACCESS_DENIED", {
833
+ membership: status
834
+ });
835
+ }
836
+ const raw = await readBody(req);
837
+ const body = raw.length ? JSON.parse(raw.toString("utf8")) : void 0;
838
+ const method = req.method;
839
+ if (path[0] === "_files")
840
+ return json(
841
+ res,
842
+ method === "POST" ? 201 : 200,
843
+ await executeMedia(method, path.slice(1), body, { member })
844
+ );
845
+ if (path[0] === "_batch") {
846
+ if (!owner) throw new DataError(403, "A batch needs an owner sign-in.");
847
+ const result2 = await object("batch", {
848
+ ownerId: OWNER,
849
+ allowedIds: null,
850
+ operations: body?.operations
851
+ });
852
+ return json(res, 201, publicResult(result2));
853
+ }
854
+ if (Object.hasOwn(CALLS, path[0])) {
855
+ const { file, fn } = functionReference(path.slice(1).join("/"));
856
+ const { result: result2 } = await object("call", {
857
+ ownerId: OWNER,
858
+ site,
859
+ member,
860
+ ...owner || member ? {} : { visitor: true },
861
+ file,
862
+ name: fn,
863
+ args: body?.args ?? null,
864
+ kind: CALLS[path[0]]
865
+ });
866
+ return json(res, 200, { result: result2 });
867
+ }
868
+ if (path[0] === "_live") {
869
+ if (!member && !owner)
870
+ throw new DataError(403, "Live tickets are for members and the owner.");
871
+ return json(res, 200, {
872
+ ticket: await signLiveTicket(secret, site, member ?? OWNER_TICKET)
873
+ });
874
+ }
875
+ if (member)
876
+ throw new DataError(
877
+ 403,
878
+ "Members reach a site's data through its queries and mutations, not its collections.",
879
+ "ACCESS_DENIED"
880
+ );
881
+ const { result } = await object("execute", {
882
+ ownerId: OWNER,
883
+ site,
884
+ access: { admin: owner, allowedIds: null, anonymous: false },
885
+ path,
886
+ method,
887
+ body,
888
+ filter: parseFilterQuery(url.searchParams.get("filter")),
889
+ size: url.searchParams.has("size") ? Number(url.searchParams.get("size")) : void 0,
890
+ sort: url.searchParams.get("sort") ?? void 0,
891
+ after: url.searchParams.get("after") ?? void 0,
892
+ includeTotal: url.searchParams.get("includeTotal") === "1",
893
+ ifVersion: websiteCondition(url)
894
+ });
895
+ return json(res, method === "POST" ? 201 : 200, publicResult(result));
896
+ }
897
+ const sdkVersions = (await readdir(resources.sdkDir)).filter((name2) => /^\d+\.\d+\.\d+$/.test(name2)).sort();
898
+ let importMap = "";
899
+ const withImportMap = (html) => /<head[^>]*>/i.test(html) ? html.replace(/<head[^>]*>/i, (head) => `${head}${importMap}`) : importMap + html;
900
+ async function sdk(res, path) {
901
+ const match = /^\/sdk\/(\d+(?:\.\d+\.\d+)?)\/(naru\.js|naru\.d\.ts|functions\.d\.ts)$/.exec(
902
+ path
903
+ );
904
+ const version = match?.[1] === "1" ? "1.0.0" : match?.[1];
905
+ if (!match || !sdkVersions.includes(version))
906
+ return res.writeHead(404).end();
907
+ const file = join(resources.sdkDir, version, match[2]);
908
+ if (!existsSync(file)) return res.writeHead(404).end();
909
+ const source = (await readFile(file, "utf8")).replaceAll("https://sdk.naru.pub", origin).replaceAll("https://naru.pub", origin).replace(
910
+ "site ||= /^([a-z0-9-]+)\\.naru\\.pub$/.exec(",
911
+ `site ||= "${site}" || /^([a-z0-9-]+)\\.naru\\.pub$/.exec(`
912
+ );
913
+ res.writeHead(200, {
914
+ "content-type": TYPES[extname(match[2])] ?? "text/plain",
915
+ "cache-control": "no-store"
916
+ }).end(source);
917
+ }
918
+ async function route(res, url) {
919
+ let rules = NO_ROUTES;
920
+ try {
921
+ rules = routeRules(
922
+ await readFile(join(folder, "_naru", "routes.json"), "utf8")
923
+ );
924
+ } catch {
925
+ }
926
+ if (!routed(rules, url.pathname)) return false;
927
+ const html = (status, body, headers = {}) => res.writeHead(status, {
928
+ "content-type": "text/html; charset=utf-8",
929
+ "cache-control": "no-store",
930
+ ...headers
931
+ }).end(withImportMap(body));
932
+ try {
933
+ const { result } = await object("render", {
934
+ site,
935
+ path: url.pathname,
936
+ origin: url.origin,
937
+ query: Object.fromEntries(url.searchParams)
938
+ });
939
+ const answer = routeAnswer(result);
940
+ if ("redirect" in answer)
941
+ res.writeHead(answer.status, { location: answer.redirect }).end();
942
+ else if ("body" in answer && answer.type === "text/html")
943
+ html(answer.status, answer.body);
944
+ else if ("body" in answer)
945
+ res.writeHead(answer.status, {
946
+ "content-type": `${answer.type}; charset=utf-8`,
947
+ "cache-control": "no-store"
948
+ }).end(answer.body);
949
+ else {
950
+ let file = resolve(folder, answer.file);
951
+ if (existsSync(file) && (await stat(file)).isDirectory())
952
+ file = join(file, "index.html");
953
+ if (!file.startsWith(folder) || !existsSync(file))
954
+ html(404, `The route's file ${answer.file} is not there.`);
955
+ else
956
+ html(
957
+ answer.status,
958
+ withHead(await readFile(file, "utf8"), answer.head)
959
+ );
960
+ }
961
+ } catch (error) {
962
+ const status = error instanceof DataError ? error.status : 500;
963
+ html(status, `<p>${error.message.replace(/</g, "&lt;")}</p>`);
964
+ }
965
+ return true;
966
+ }
967
+ async function page(res, path) {
968
+ let file = resolve(folder, `.${decodeURIComponent(path)}`);
969
+ if (!(file === folder || file.startsWith(folder + sep)) || relative(folder, file).split(sep).includes(".naru-dev") || // As on Naru, which serves nothing under _naru/.
970
+ relative(folder, file).split(sep)[0] === "_naru" || file.startsWith(state + sep))
971
+ return res.writeHead(404).end();
972
+ if (existsSync(file) && (await stat(file)).isDirectory())
973
+ file = join(file, "index.html");
974
+ else if (!existsSync(file) && existsSync(`${file}.html`))
975
+ file = `${file}.html`;
976
+ if (!existsSync(file)) return res.writeHead(404).end("Not found.");
977
+ const type = TYPES[extname(file)] ?? "application/octet-stream";
978
+ let content = await readFile(file);
979
+ if (type.startsWith("text/html"))
980
+ content = withImportMap(content.toString("utf8"));
981
+ res.writeHead(200, { "content-type": type, "cache-control": "no-store" }).end(content);
982
+ }
983
+ async function toWorker(req, res, url) {
984
+ const body = ["GET", "HEAD"].includes(req.method) ? void 0 : await readBody(req);
985
+ const headers = new Headers();
986
+ for (const [key2, value] of Object.entries(req.headers))
987
+ if (typeof value === "string" && key2 !== "host") headers.set(key2, value);
988
+ const answer = await fetch(`${workerUrl}${url.pathname}${url.search}`, {
989
+ method: req.method,
990
+ headers,
991
+ body
992
+ });
993
+ const headersOut = Object.fromEntries(
994
+ [...answer.headers].filter(
995
+ ([key2]) => !["content-encoding", "content-length", "transfer-encoding"].includes(
996
+ key2
997
+ )
998
+ )
999
+ );
1000
+ res.writeHead(answer.status, headersOut);
1001
+ res.end(Buffer.from(await answer.arrayBuffer()));
1002
+ }
1003
+ async function handle(req, res) {
1004
+ const url = new URL(req.url, origin);
1005
+ if (url.pathname.startsWith("/_naru-dev/bucket/"))
1006
+ return await bucket(req, res, url);
1007
+ await started;
1008
+ if (url.pathname.startsWith("/sdk/")) return await sdk(res, url.pathname);
1009
+ if (url.pathname === "/database/authorize")
1010
+ return await ownerSignIn(res, url.searchParams);
1011
+ if (url.pathname === "/members/sign-in")
1012
+ return await memberSignIn(res, url.searchParams);
1013
+ if (req.method === "POST" && ["/api/data-auth/v1/token", "/api/data-auth/v1/member-token"].includes(
1014
+ url.pathname
1015
+ )) {
1016
+ const body = JSON.parse((await readBody(req)).toString("utf8"));
1017
+ const grant = pending.get(body.code);
1018
+ pending.delete(body.code);
1019
+ if (!grant || grant.challenge !== digest(body.verifier) || grant.redirectUri !== body.redirectUri)
1020
+ throw new DataError(
1021
+ 401,
1022
+ "The sign-in could not be completed.",
1023
+ "AUTH_REQUIRED"
1024
+ );
1025
+ return json(res, 200, {
1026
+ accessToken: await issue(grant.member),
1027
+ expiresIn: LIFETIME,
1028
+ ...grant.member ? { memberId: grant.member } : {}
1029
+ });
1030
+ }
1031
+ if (url.pathname.startsWith("/api/data-auth/v1/"))
1032
+ return json(res, 200, { success: true });
1033
+ if (url.pathname === "/_naru-dev/media-worker.js")
1034
+ return res.writeHead(200, {
1035
+ "content-type": "text/javascript",
1036
+ "service-worker-allowed": "/"
1037
+ }).end(serviceWorker);
1038
+ if (url.pathname.startsWith("/_naru-dev/upload/") && req.method === "PUT") {
1039
+ const upload = uploads.get(url.pathname.split("/").pop());
1040
+ if (!upload) return res.writeHead(404).end();
1041
+ await writeFile(join(mediaDir, upload.file), await readBody(req));
1042
+ return res.writeHead(200).end();
1043
+ }
1044
+ if (url.pathname.startsWith(`/media/${OWNER}/`)) {
1045
+ const upload = [...uploads.values()].find(
1046
+ (row) => row.file === url.pathname.split("/").pop()
1047
+ );
1048
+ if (!upload || !existsSync(join(mediaDir, upload.file)))
1049
+ return res.writeHead(404).end();
1050
+ return res.writeHead(200, { "content-type": upload.contentType }).end(await readFile(join(mediaDir, upload.file)));
1051
+ }
1052
+ const api = /^\/api\/data\/v1\/([^/]+)(?:\/(.*))?$/.exec(url.pathname);
1053
+ if (api) {
1054
+ if (api[1] !== site)
1055
+ throw new DataError(404, `naru-dev serves only ${site}.`);
1056
+ const path = (api[2] ?? "").split("/").filter(Boolean).map(decodeURIComponent);
1057
+ if (!req.headers.authorization && !["_files", "_batch", "_live"].includes(path[0] ?? ""))
1058
+ return await toWorker(req, res, url);
1059
+ return await signed(req, res, url, path);
1060
+ }
1061
+ if (["GET", "HEAD"].includes(req.method) && await route(res, url))
1062
+ return;
1063
+ return await page(res, url.pathname);
1064
+ }
1065
+ const server = createServer((req, res) => {
1066
+ handle(req, res).catch((error) => failure(res, error));
1067
+ });
1068
+ const live = /* @__PURE__ */ new Set();
1069
+ server.on("upgrade", (req, socket, head) => {
1070
+ live.add(socket);
1071
+ socket.once("close", () => live.delete(socket));
1072
+ const upstream = connect(workerPort, "127.0.0.1", () => {
1073
+ upstream.write(
1074
+ `${req.method} ${req.url} HTTP/1.1\r
1075
+ ` + Object.entries(req.headers).map(
1076
+ ([key2, value]) => key2 === "host" ? `host: 127.0.0.1:${workerPort}` : `${key2}: ${value}`
1077
+ ).join("\r\n") + "\r\n\r\n"
1078
+ );
1079
+ upstream.write(head);
1080
+ socket.pipe(upstream).pipe(socket);
1081
+ });
1082
+ upstream.on("error", () => socket.destroy());
1083
+ socket.on("error", () => upstream.destroy());
1084
+ });
1085
+ const port = await new Promise((done, fail) => {
1086
+ server.once("error", fail);
1087
+ server.listen(options.port ?? 0, () => {
1088
+ server.off("error", fail);
1089
+ done(server.address().port);
1090
+ });
1091
+ });
1092
+ origin = `http://localhost:${port}`;
1093
+ importMap = `<script>navigator.serviceWorker?.register("/_naru-dev/media-worker.js",{scope:"/"})</script><script type="importmap">${JSON.stringify(
1094
+ {
1095
+ imports: Object.fromEntries(
1096
+ ["1", ...sdkVersions].flatMap(
1097
+ (version) => ["https://sdk.naru.pub", "https://naru.pub"].map((host) => [
1098
+ `${host}/sdk/${version}/naru.js`,
1099
+ `${origin}/sdk/${version}/naru.js`
1100
+ ])
1101
+ )
1102
+ )
1103
+ }
1104
+ )}</script>`;
1105
+ let watcher = null;
1106
+ const stop = async () => {
1107
+ if (stopping) return;
1108
+ stopping = true;
1109
+ watcher?.close();
1110
+ clearTimeout(settling);
1111
+ const closed = new Promise((done) => server.close(done));
1112
+ server.closeAllConnections();
1113
+ for (const socket of live) socket.destroy();
1114
+ if (worker && worker.exitCode === null && worker.signalCode === null) {
1115
+ const exited = new Promise((done) => worker.once("exit", done));
1116
+ worker.kill();
1117
+ await exited;
1118
+ }
1119
+ await closed;
1120
+ if (madeState) await rm(state, { recursive: true, force: true });
1121
+ };
1122
+ try {
1123
+ await startWorker();
1124
+ await install();
1125
+ } catch (error) {
1126
+ await stop();
1127
+ throw error;
1128
+ }
1129
+ if (existsSync(join(folder, "_naru")))
1130
+ watcher = watch(
1131
+ join(folder, "_naru"),
1132
+ { recursive: true },
1133
+ (_event, name2) => {
1134
+ if (!name2) return;
1135
+ const file = name2.toString();
1136
+ if (file === "collections.json") void installDeclaration();
1137
+ else republish(join(folder, "_naru", file));
1138
+ }
1139
+ );
1140
+ ready();
1141
+ log(
1142
+ `naru-dev: ${site} at ${origin}/ (owner and member sign-in are one click; data in ${shortest(state)})`
1143
+ );
1144
+ return {
1145
+ origin,
1146
+ name: site,
1147
+ state,
1148
+ get log() {
1149
+ return output;
1150
+ },
1151
+ url: (path = "/") => new URL(path, origin).href,
1152
+ async signIn(target, who, signInOptions = {}) {
1153
+ if (typeof who !== "string" || !who.trim())
1154
+ throw new TypeError(`who is "owner" or a member's name.`);
1155
+ const member = who === "owner" ? void 0 : await keepMember(who.trim(), signInOptions.status);
1156
+ if (!member && signInOptions.collections)
1157
+ await ownerCollections(signInOptions.collections);
1158
+ const accessToken = await issue(member);
1159
+ const credentials = {
1160
+ accessToken,
1161
+ expiresAt: Date.now() + LIFETIME * 1e3,
1162
+ ...member ? { memberId: member } : {},
1163
+ headers: { authorization: `Bearer ${accessToken}` }
1164
+ };
1165
+ if (target) {
1166
+ const script = `(() => {
1167
+ const s = ${JSON.stringify({
1168
+ origin,
1169
+ site,
1170
+ owner: !member,
1171
+ id: randomUUID(),
1172
+ session: {
1173
+ accessToken,
1174
+ expiresAt: credentials.expiresAt,
1175
+ ...member ? { memberId: member } : {}
1176
+ }
1177
+ })};
1178
+ try {
1179
+ if (location.origin !== s.origin) return;
1180
+ const stores = s.owner ? [sessionStorage, localStorage] : [localStorage];
1181
+ const key = s.owner
1182
+ ? "naru:owner:" + s.origin + ":" + s.site + ":" + location.origin + location.pathname
1183
+ : "naru:member:" + s.origin + ":" + s.site;
1184
+ const mark = "naru-dev:signed-in:" + s.id + ":" + (s.owner ? location.pathname : "");
1185
+ if (sessionStorage.getItem(mark)) return;
1186
+ sessionStorage.setItem(mark, "1");
1187
+ for (const store of stores) store.setItem(key, JSON.stringify(s.session));
1188
+ } catch {}
1189
+ })();`;
1190
+ await target.addInitScript(script);
1191
+ const here2 = typeof target.url === "function" ? target.url() : "";
1192
+ if (typeof target.evaluate === "function" && here2.startsWith(origin))
1193
+ await target.evaluate(script);
1194
+ }
1195
+ return credentials;
1196
+ },
1197
+ async reset() {
1198
+ await object("erase", {});
1199
+ uploads.clear();
1200
+ await saveUploads();
1201
+ await rm(mediaDir, { recursive: true, force: true });
1202
+ await mkdir(mediaDir, { recursive: true });
1203
+ await install();
1204
+ },
1205
+ stop
1206
+ };
1207
+ }
1208
+
1209
+ // src/packaged.ts
1210
+ import { readFileSync } from "node:fs";
1211
+ import { createRequire } from "node:module";
1212
+ import { dirname as dirname2, join as join2 } from "node:path";
1213
+ import { fileURLToPath } from "node:url";
1214
+ var here = dirname2(fileURLToPath(import.meta.url));
1215
+ function packagedResources() {
1216
+ const require2 = createRequire(import.meta.url);
1217
+ const workerd = require2("workerd");
1218
+ return {
1219
+ workerDir: join2(here, "worker"),
1220
+ sdkDir: join2(here, "sdk"),
1221
+ workerd: workerd.default
1222
+ };
1223
+ }
1224
+ var packagedInfo = () => JSON.parse(readFileSync(join2(here, "build.json"), "utf8"));
1225
+
1226
+ export {
1227
+ start,
1228
+ packagedResources,
1229
+ packagedInfo
1230
+ };