@micacms/runtime 0.5.0-rc.2 → 0.6.0-rc.1
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/README.md +3 -1
- package/dist/app.js +1 -1
- package/dist/app.js.map +1 -1
- package/dist/config.d.ts +13 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -9
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +9 -3
- package/dist/context.js.map +1 -1
- package/dist/http.d.ts +1 -3
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +2 -9
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/errors.d.ts +1 -2
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +0 -2
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/id.d.ts +0 -10
- package/dist/lib/id.d.ts.map +1 -1
- package/dist/lib/id.js +0 -50
- package/dist/lib/id.js.map +1 -1
- package/dist/lib/rate-limit.d.ts +0 -2
- package/dist/lib/rate-limit.d.ts.map +1 -1
- package/dist/lib/rate-limit.js +0 -3
- package/dist/lib/rate-limit.js.map +1 -1
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/index.js.map +1 -1
- package/dist/routes/admin-content.d.ts.map +1 -1
- package/dist/routes/admin-content.js +4 -3
- package/dist/routes/admin-content.js.map +1 -1
- package/dist/routes/admin-media.d.ts.map +1 -1
- package/dist/routes/admin-media.js +6 -1
- package/dist/routes/admin-media.js.map +1 -1
- package/dist/routes/admin-system.d.ts.map +1 -1
- package/dist/routes/admin-system.js +6 -4
- package/dist/routes/admin-system.js.map +1 -1
- package/dist/schema.d.ts +15 -3
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +63 -10
- package/dist/schema.js.map +1 -1
- package/dist/services/changes.d.ts.map +1 -1
- package/dist/services/changes.js +89 -13
- package/dist/services/changes.js.map +1 -1
- package/dist/services/content-store.d.ts +6 -1
- package/dist/services/content-store.d.ts.map +1 -1
- package/dist/services/content-store.js +25 -15
- package/dist/services/content-store.js.map +1 -1
- package/dist/services/staging.d.ts +2 -0
- package/dist/services/staging.d.ts.map +1 -1
- package/dist/services/staging.js +15 -4
- package/dist/services/staging.js.map +1 -1
- package/dist/workspace/tree.d.ts +6 -0
- package/dist/workspace/tree.d.ts.map +1 -1
- package/dist/workspace/tree.js +28 -0
- package/dist/workspace/tree.js.map +1 -1
- package/package.json +12 -4
- package/src/app.ts +127 -0
- package/src/config.ts +113 -0
- package/src/context.ts +167 -0
- package/src/http.ts +46 -0
- package/src/index.ts +141 -0
- package/src/lib/errors.ts +84 -0
- package/src/lib/file-type.ts +108 -0
- package/src/lib/id.ts +6 -0
- package/src/lib/logger.ts +87 -0
- package/src/lib/ports.ts +17 -0
- package/src/lib/rate-limit.ts +72 -0
- package/src/lib/schema-stats.ts +183 -0
- package/src/lib/version.ts +29 -0
- package/src/middleware/index.ts +234 -0
- package/src/routes/admin-content.ts +197 -0
- package/src/routes/admin-media.ts +96 -0
- package/src/routes/admin-spa.ts +82 -0
- package/src/routes/admin-staging.ts +126 -0
- package/src/routes/admin-system.ts +146 -0
- package/src/routes/health.ts +67 -0
- package/src/schema.ts +217 -0
- package/src/server.ts +68 -0
- package/src/services/changes.ts +605 -0
- package/src/services/content-store.ts +492 -0
- package/src/services/media.ts +229 -0
- package/src/services/staging.ts +330 -0
- package/src/workspace/developer.ts +46 -0
- package/src/workspace/files.ts +243 -0
- package/src/workspace/git.ts +108 -0
- package/src/workspace/locks.ts +103 -0
- package/src/workspace/policy.ts +77 -0
- package/src/workspace/tree.ts +314 -0
package/dist/lib/id.js
CHANGED
|
@@ -1,54 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ULID generation.
|
|
3
|
-
*
|
|
4
|
-
* Lexicographically sortable, so `ORDER BY id` matches creation order and
|
|
5
|
-
* pagination stays stable without a secondary sort. This is an identifier
|
|
6
|
-
* encoding, not a cryptographic construction — the randomness comes from
|
|
7
|
-
* `crypto.randomBytes`.
|
|
8
|
-
*/
|
|
9
1
|
import { randomBytes } from "node:crypto";
|
|
10
|
-
const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford base32
|
|
11
|
-
const TIME_CHARS = 10;
|
|
12
|
-
const RANDOM_CHARS = 16;
|
|
13
|
-
let lastTime = 0;
|
|
14
|
-
let lastRandom = [];
|
|
15
|
-
function encodeTime(time) {
|
|
16
|
-
let out = "";
|
|
17
|
-
for (let i = TIME_CHARS - 1; i >= 0; i--) {
|
|
18
|
-
out = ALPHABET[time % 32] + out;
|
|
19
|
-
time = Math.floor(time / 32);
|
|
20
|
-
}
|
|
21
|
-
return out;
|
|
22
|
-
}
|
|
23
|
-
function randomChars() {
|
|
24
|
-
const bytes = randomBytes(RANDOM_CHARS);
|
|
25
|
-
return Array.from(bytes, (byte) => byte % 32);
|
|
26
|
-
}
|
|
27
|
-
/** Increments the random component so ids created in the same millisecond still sort. */
|
|
28
|
-
function bumpRandom(chars) {
|
|
29
|
-
const next = [...chars];
|
|
30
|
-
for (let i = next.length - 1; i >= 0; i--) {
|
|
31
|
-
if (next[i] < 31) {
|
|
32
|
-
next[i] = next[i] + 1;
|
|
33
|
-
return next;
|
|
34
|
-
}
|
|
35
|
-
next[i] = 0;
|
|
36
|
-
}
|
|
37
|
-
return randomChars();
|
|
38
|
-
}
|
|
39
|
-
export function ulid(now = Date.now()) {
|
|
40
|
-
if (now === lastTime) {
|
|
41
|
-
lastRandom = bumpRandom(lastRandom);
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
lastTime = now;
|
|
45
|
-
lastRandom = randomChars();
|
|
46
|
-
}
|
|
47
|
-
return encodeTime(now) + lastRandom.map((index) => ALPHABET[index]).join("");
|
|
48
|
-
}
|
|
49
|
-
export function isUlid(value) {
|
|
50
|
-
return value.length === 26 && [...value].every((char) => ALPHABET.includes(char));
|
|
51
|
-
}
|
|
52
2
|
/** URL-safe random token for sessions and one-time links. */
|
|
53
3
|
export function randomToken(bytes = 32) {
|
|
54
4
|
return randomBytes(bytes).toString("base64url");
|
package/dist/lib/id.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/lib/id.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/lib/id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,6DAA6D;AAC7D,MAAM,UAAU,WAAW,CAAC,KAAK,GAAG,EAAE;IACpC,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAClD,CAAC","sourcesContent":["import { randomBytes } from \"node:crypto\";\n\n/** URL-safe random token for sessions and one-time links. */\nexport function randomToken(bytes = 32): string {\n return randomBytes(bytes).toString(\"base64url\");\n}\n"]}
|
package/dist/lib/rate-limit.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/lib/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC,
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/lib/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC,IAAI,IAAI,MAAM,CAAC;IACf,IAAI,IAAI,IAAI,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CA0C1E"}
|
package/dist/lib/rate-limit.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/lib/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/lib/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAsBH,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,OAAO,GAAG,IAAI,GAAG,EAA8C,CAAC;IAEtE,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACpC,IAAI,MAAM,CAAC,OAAO,IAAI,GAAG;gBAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjD,CAAC;IACH,CAAC,EAAE,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IAE1D,6DAA6D;IAC7D,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IAEhB,OAAO;QACL,KAAK,CAAC,GAAG;YACP,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAEhC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,IAAI,GAAG,EAAE,CAAC;gBACrC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;gBAChE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;YACxE,CAAC;YAED,MAAM,CAAC,KAAK,EAAE,CAAC;YACf,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;YAE5D,IAAI,MAAM,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;gBACjC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC;YACtD,CAAC;YAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,CAAC;QAChF,CAAC;QAED,IAAI;YACF,OAAO,OAAO,CAAC,IAAI,CAAC;QACtB,CAAC;QAED,IAAI;YACF,aAAa,CAAC,KAAK,CAAC,CAAC;YACrB,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Fixed-window rate limiting, in memory.\n *\n * The CMS is a single process by design, so an in-memory counter is\n * sufficient and avoids a Redis dependency. The trade-off is deliberate:\n * counters reset on restart. Mica checks no passwords; the identity provider\n * does, with its own limits, so a restart-and-retry loop gains little.\n */\n\nexport interface RateLimitResult {\n allowed: boolean;\n remaining: number;\n /** Seconds until the window resets. */\n retryAfter: number;\n}\n\nexport interface RateLimiter {\n check(key: string): RateLimitResult;\n size(): number;\n stop(): void;\n}\n\nexport interface RateLimiterOptions {\n limit: number;\n windowMs: number;\n /** How often expired buckets are swept. */\n sweepMs?: number;\n}\n\nexport function createRateLimiter(options: RateLimiterOptions): RateLimiter {\n const buckets = new Map<string, { count: number; resetAt: number }>();\n\n const sweep = setInterval(() => {\n const now = Date.now();\n for (const [key, bucket] of buckets) {\n if (bucket.resetAt <= now) buckets.delete(key);\n }\n }, options.sweepMs ?? Math.max(options.windowMs, 60_000));\n\n // A sweep timer must not keep the process alive on shutdown.\n sweep.unref?.();\n\n return {\n check(key) {\n const now = Date.now();\n const bucket = buckets.get(key);\n\n if (!bucket || bucket.resetAt <= now) {\n buckets.set(key, { count: 1, resetAt: now + options.windowMs });\n return { allowed: true, remaining: options.limit - 1, retryAfter: 0 };\n }\n\n bucket.count++;\n const retryAfter = Math.ceil((bucket.resetAt - now) / 1000);\n\n if (bucket.count > options.limit) {\n return { allowed: false, remaining: 0, retryAfter };\n }\n\n return { allowed: true, remaining: options.limit - bucket.count, retryAfter };\n },\n\n size() {\n return buckets.size;\n },\n\n stop() {\n clearInterval(sweep);\n buckets.clear();\n },\n };\n}\n"]}
|
|
@@ -25,7 +25,7 @@ export declare const MAX_FILES_PER_UPLOAD = 10;
|
|
|
25
25
|
export declare const MAX_JSON_BYTES: number;
|
|
26
26
|
/**
|
|
27
27
|
* Refuses an oversized body before a handler reads it into memory (#148). An
|
|
28
|
-
* upload may carry `MAX_FILES_PER_UPLOAD` files of `
|
|
28
|
+
* upload may carry `MAX_FILES_PER_UPLOAD` files of `MICA_MAX_UPLOAD_BYTES`, plus
|
|
29
29
|
* room for the multipart framing; every other request gets `MAX_JSON_BYTES`.
|
|
30
30
|
*/
|
|
31
31
|
export declare function requestSizeLimit(): MiddlewareHandler<Env>;
|
package/dist/middleware/index.js
CHANGED
|
@@ -86,7 +86,7 @@ export const MAX_FILES_PER_UPLOAD = 10;
|
|
|
86
86
|
export const MAX_JSON_BYTES = 2 * 1024 * 1024;
|
|
87
87
|
/**
|
|
88
88
|
* Refuses an oversized body before a handler reads it into memory (#148). An
|
|
89
|
-
* upload may carry `MAX_FILES_PER_UPLOAD` files of `
|
|
89
|
+
* upload may carry `MAX_FILES_PER_UPLOAD` files of `MICA_MAX_UPLOAD_BYTES`, plus
|
|
90
90
|
* room for the multipart framing; every other request gets `MAX_JSON_BYTES`.
|
|
91
91
|
*/
|
|
92
92
|
export function requestSizeLimit() {
|
|
@@ -94,7 +94,7 @@ export function requestSizeLimit() {
|
|
|
94
94
|
const { config } = c.get("ctx");
|
|
95
95
|
const upload = c.req.method === "POST" && c.req.path === "/api/admin/media";
|
|
96
96
|
const maxSize = upload
|
|
97
|
-
? config.
|
|
97
|
+
? config.MICA_MAX_UPLOAD_BYTES * MAX_FILES_PER_UPLOAD + 1024 * 1024
|
|
98
98
|
: MAX_JSON_BYTES;
|
|
99
99
|
return bodyLimit({
|
|
100
100
|
maxSize,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Middleware.\n *\n * Ordering matters and is asserted in `app.ts`:\n * requestContext → guards → securityHeaders → errorBoundary → rateLimit\n * → originGuard → the auth surface's gate → route\n *\n * The gate itself is not here. Whether a request is anybody — a session\n * middleware, `allowAll()` — belongs to the composing app, which is what\n * keeps this package authentication-neutral.\n */\n\nimport { ContentPathError } from \"@micacms/core\";\nimport type { ErrorHandler, MiddlewareHandler } from \"hono\";\nimport { bodyLimit } from \"hono/body-limit\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\n\nimport { ApiError, badRequest } from \"../lib/errors.js\";\nimport { clientIp, type Env } from \"../http.js\";\nimport { randomToken } from \"../lib/id.js\";\n\n/** Attaches the container, a request id and a scoped logger; logs the result. */\nexport function requestContext(ctx: Env[\"Variables\"][\"ctx\"]): MiddlewareHandler<Env> {\n return async (c, next) => {\n // A client's id is logged and echoed, so only a short, plain one is kept (#150).\n const offered = c.req.header(\"x-request-id\");\n const requestId = offered && /^[A-Za-z0-9_-]{1,64}$/.test(offered) ? offered : randomToken(8);\n const logger = ctx.logger.child({ requestId });\n\n c.set(\"ctx\", ctx);\n c.set(\"requestId\", requestId);\n c.set(\"logger\", logger);\n c.header(\"x-request-id\", requestId);\n\n const startedAt = performance.now();\n await next();\n const duration = Math.round(performance.now() - startedAt);\n\n const line = {\n method: c.req.method,\n path: new URL(c.req.url).pathname,\n status: c.res.status,\n duration,\n };\n\n /**\n * \"Am I signed in?\" — \"No.\"\n *\n * `/auth/me` is the SPA's first request on every page load, and a 401 is\n * one of its two correct answers, not a rejection worth a warning. Logging\n * it as one puts a scary line in the terminal of everybody who has simply\n * opened the editor while signed out.\n */\n const expected401 = c.res.status === 401 && line.path === \"/api/auth/me\";\n\n if (c.res.status >= 500) logger.error(\"Request failed\", line);\n else if (c.res.status >= 400 && !expected401) logger.warn(\"Request rejected\", line);\n else logger.debug(\"Request\", line);\n };\n}\n\n/**\n * Same-origin only. `blob:` and `data:` images cover previews; a route that\n * needs a stricter policy, such as the sandbox on media files, sets its own.\n */\nexport const CONTENT_SECURITY_POLICY = [\n \"default-src 'self'\",\n \"img-src 'self' blob: data:\",\n \"object-src 'none'\",\n \"base-uri 'none'\",\n \"frame-ancestors 'none'\",\n].join(\"; \");\n\nexport function securityHeaders(): MiddlewareHandler<Env> {\n return async (c, next) => {\n await next();\n\n if (!c.res.headers.has(\"content-security-policy\")) {\n c.header(\"content-security-policy\", CONTENT_SECURITY_POLICY);\n }\n // Only for an https public URL: HSTS over plain HTTP is ignored, and on\n // localhost it would pin a developer's other projects to https.\n if (c.get(\"ctx\").config.MICA_PUBLIC_URL.startsWith(\"https://\")) {\n c.header(\"strict-transport-security\", \"max-age=31536000\");\n }\n\n c.header(\"x-content-type-options\", \"nosniff\");\n c.header(\"referrer-policy\", \"same-origin\");\n c.header(\"x-frame-options\", \"DENY\");\n // The admin is a first-party SPA; nothing here needs to be embedded.\n c.header(\"permissions-policy\", \"geolocation=(), microphone=(), camera=()\");\n };\n}\n\n/** Files in one upload request; the Media view sends a selection in one request. */\nexport const MAX_FILES_PER_UPLOAD = 10;\n\n/** Far more than any entry's JSON, and small enough to refuse a flood early. */\nexport const MAX_JSON_BYTES = 2 * 1024 * 1024;\n\n/**\n * Refuses an oversized body before a handler reads it into memory (#148). An\n * upload may carry `MAX_FILES_PER_UPLOAD` files of `MAX_UPLOAD_BYTES`, plus\n * room for the multipart framing; every other request gets `MAX_JSON_BYTES`.\n */\nexport function requestSizeLimit(): MiddlewareHandler<Env> {\n return async (c, next) => {\n const { config } = c.get(\"ctx\");\n const upload = c.req.method === \"POST\" && c.req.path === \"/api/admin/media\";\n const maxSize = upload\n ? config.MAX_UPLOAD_BYTES * MAX_FILES_PER_UPLOAD + 1024 * 1024\n : MAX_JSON_BYTES;\n\n return bodyLimit({\n maxSize,\n onError: (ctx) => {\n const refused = new ApiError(\n \"payload_too_large\",\n upload\n ? \"The upload is too large. Upload fewer or smaller files at a time.\"\n : \"The request is too large.\",\n );\n return ctx.json(refused.toBody(), 413);\n },\n })(c, next);\n };\n}\n\n/**\n * Converts thrown errors into the API error envelope. Deliberate failures\n * (`ApiError`) are reported as-is; anything else is a bug and becomes a bare\n * 500, with the stack kept in the log rather than sent to the client.\n *\n * Registered with `app.onError`, not as middleware: Hono's `compose` wraps each\n * handler individually and routes a throw to `onError` at that level, so a\n * try/catch in an outer middleware would never see it.\n */\nexport function errorHandler(): ErrorHandler<Env> {\n return (error, c) => {\n const logger = c.get(\"logger\") ?? c.get(\"ctx\").logger;\n\n if (error instanceof ApiError) {\n logger.debug(\"Handled error\", { code: error.code, message: error.message });\n return c.json(error.toBody(), error.status as ContentfulStatusCode, error.headers);\n }\n\n // A path that could leave the content root is the client's mistake, not a bug.\n if (error instanceof ContentPathError) {\n logger.warn(\"Refused content path\", { message: error.message });\n const refused = badRequest(error.message);\n return c.json(refused.toBody(), refused.status as ContentfulStatusCode);\n }\n\n logger.error(\"Unhandled error\", {\n error: error instanceof Error ? (error.stack ?? error.message) : String(error),\n path: new URL(c.req.url).pathname,\n });\n\n return c.json(\n {\n error: {\n code: \"internal_error\" as const,\n message: \"Something went wrong. Please try again.\",\n requestId: c.get(\"requestId\"),\n },\n },\n 500,\n );\n };\n}\n\n/** Coarse per-IP limit, applied to the whole API surface. */\nexport function rateLimit(): MiddlewareHandler<Env> {\n return async (c, next) => {\n const { limiters } = c.get(\"ctx\");\n const result = limiters.api.check(clientIp(c));\n\n if (!result.allowed) {\n throw new ApiError(\"rate_limited\", \"Too many requests. Please slow down.\", {\n headers: { \"retry-after\": String(result.retryAfter) },\n });\n }\n\n await next();\n };\n}\n\nconst SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\nconst normaliseOrigin = (value: string): string => {\n try {\n return new URL(value).origin;\n } catch {\n return value;\n }\n};\n\nexport function allowedOrigins(config: {\n MICA_PUBLIC_URL: string;\n adminOrigins: string[];\n}): string[] {\n return [...new Set([config.MICA_PUBLIC_URL, ...config.adminOrigins].map(normaliseOrigin))];\n}\n\n/**\n * First CSRF layer: the browser sets `Origin` on every state-changing request\n * and a page on another site cannot forge it.\n *\n * Applied to the whole API — including sign-in, which has no session yet and\n * so cannot use the token check below.\n */\nexport function originGuard(): MiddlewareHandler<Env> {\n return async (c, next) => {\n if (SAFE_METHODS.has(c.req.method)) {\n await next();\n return;\n }\n\n const { config } = c.get(\"ctx\");\n const origin = c.req.header(\"origin\");\n\n if (origin) {\n if (!allowedOrigins(config).includes(normaliseOrigin(origin))) {\n throw new ApiError(\"forbidden\", \"Request origin is not allowed.\");\n }\n } else if (config.isProduction) {\n // A browser always sends Origin on writes; its absence means the request\n // did not come from one. Tolerated in development for curl-based testing.\n throw new ApiError(\"forbidden\", \"Missing Origin header.\");\n }\n\n await next();\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Middleware.\n *\n * Ordering matters and is asserted in `app.ts`:\n * requestContext → guards → securityHeaders → errorBoundary → rateLimit\n * → originGuard → the auth surface's gate → route\n *\n * The gate itself is not here. Whether a request is anybody — a session\n * middleware, `allowAll()` — belongs to the composing app, which is what\n * keeps this package authentication-neutral.\n */\n\nimport { ContentPathError } from \"@micacms/core\";\nimport type { ErrorHandler, MiddlewareHandler } from \"hono\";\nimport { bodyLimit } from \"hono/body-limit\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\n\nimport { ApiError, badRequest } from \"../lib/errors.js\";\nimport { clientIp, type Env } from \"../http.js\";\nimport { randomToken } from \"../lib/id.js\";\n\n/** Attaches the container, a request id and a scoped logger; logs the result. */\nexport function requestContext(ctx: Env[\"Variables\"][\"ctx\"]): MiddlewareHandler<Env> {\n return async (c, next) => {\n // A client's id is logged and echoed, so only a short, plain one is kept (#150).\n const offered = c.req.header(\"x-request-id\");\n const requestId = offered && /^[A-Za-z0-9_-]{1,64}$/.test(offered) ? offered : randomToken(8);\n const logger = ctx.logger.child({ requestId });\n\n c.set(\"ctx\", ctx);\n c.set(\"requestId\", requestId);\n c.set(\"logger\", logger);\n c.header(\"x-request-id\", requestId);\n\n const startedAt = performance.now();\n await next();\n const duration = Math.round(performance.now() - startedAt);\n\n const line = {\n method: c.req.method,\n path: new URL(c.req.url).pathname,\n status: c.res.status,\n duration,\n };\n\n /**\n * \"Am I signed in?\" — \"No.\"\n *\n * `/auth/me` is the SPA's first request on every page load, and a 401 is\n * one of its two correct answers, not a rejection worth a warning. Logging\n * it as one puts a scary line in the terminal of everybody who has simply\n * opened the editor while signed out.\n */\n const expected401 = c.res.status === 401 && line.path === \"/api/auth/me\";\n\n if (c.res.status >= 500) logger.error(\"Request failed\", line);\n else if (c.res.status >= 400 && !expected401) logger.warn(\"Request rejected\", line);\n else logger.debug(\"Request\", line);\n };\n}\n\n/**\n * Same-origin only. `blob:` and `data:` images cover previews; a route that\n * needs a stricter policy, such as the sandbox on media files, sets its own.\n */\nexport const CONTENT_SECURITY_POLICY = [\n \"default-src 'self'\",\n \"img-src 'self' blob: data:\",\n \"object-src 'none'\",\n \"base-uri 'none'\",\n \"frame-ancestors 'none'\",\n].join(\"; \");\n\nexport function securityHeaders(): MiddlewareHandler<Env> {\n return async (c, next) => {\n await next();\n\n if (!c.res.headers.has(\"content-security-policy\")) {\n c.header(\"content-security-policy\", CONTENT_SECURITY_POLICY);\n }\n // Only for an https public URL: HSTS over plain HTTP is ignored, and on\n // localhost it would pin a developer's other projects to https.\n if (c.get(\"ctx\").config.MICA_PUBLIC_URL.startsWith(\"https://\")) {\n c.header(\"strict-transport-security\", \"max-age=31536000\");\n }\n\n c.header(\"x-content-type-options\", \"nosniff\");\n c.header(\"referrer-policy\", \"same-origin\");\n c.header(\"x-frame-options\", \"DENY\");\n // The admin is a first-party SPA; nothing here needs to be embedded.\n c.header(\"permissions-policy\", \"geolocation=(), microphone=(), camera=()\");\n };\n}\n\n/** Files in one upload request; the Media view sends a selection in one request. */\nexport const MAX_FILES_PER_UPLOAD = 10;\n\n/** Far more than any entry's JSON, and small enough to refuse a flood early. */\nexport const MAX_JSON_BYTES = 2 * 1024 * 1024;\n\n/**\n * Refuses an oversized body before a handler reads it into memory (#148). An\n * upload may carry `MAX_FILES_PER_UPLOAD` files of `MICA_MAX_UPLOAD_BYTES`, plus\n * room for the multipart framing; every other request gets `MAX_JSON_BYTES`.\n */\nexport function requestSizeLimit(): MiddlewareHandler<Env> {\n return async (c, next) => {\n const { config } = c.get(\"ctx\");\n const upload = c.req.method === \"POST\" && c.req.path === \"/api/admin/media\";\n const maxSize = upload\n ? config.MICA_MAX_UPLOAD_BYTES * MAX_FILES_PER_UPLOAD + 1024 * 1024\n : MAX_JSON_BYTES;\n\n return bodyLimit({\n maxSize,\n onError: (ctx) => {\n const refused = new ApiError(\n \"payload_too_large\",\n upload\n ? \"The upload is too large. Upload fewer or smaller files at a time.\"\n : \"The request is too large.\",\n );\n return ctx.json(refused.toBody(), 413);\n },\n })(c, next);\n };\n}\n\n/**\n * Converts thrown errors into the API error envelope. Deliberate failures\n * (`ApiError`) are reported as-is; anything else is a bug and becomes a bare\n * 500, with the stack kept in the log rather than sent to the client.\n *\n * Registered with `app.onError`, not as middleware: Hono's `compose` wraps each\n * handler individually and routes a throw to `onError` at that level, so a\n * try/catch in an outer middleware would never see it.\n */\nexport function errorHandler(): ErrorHandler<Env> {\n return (error, c) => {\n const logger = c.get(\"logger\") ?? c.get(\"ctx\").logger;\n\n if (error instanceof ApiError) {\n logger.debug(\"Handled error\", { code: error.code, message: error.message });\n return c.json(error.toBody(), error.status as ContentfulStatusCode, error.headers);\n }\n\n // A path that could leave the content root is the client's mistake, not a bug.\n if (error instanceof ContentPathError) {\n logger.warn(\"Refused content path\", { message: error.message });\n const refused = badRequest(error.message);\n return c.json(refused.toBody(), refused.status as ContentfulStatusCode);\n }\n\n logger.error(\"Unhandled error\", {\n error: error instanceof Error ? (error.stack ?? error.message) : String(error),\n path: new URL(c.req.url).pathname,\n });\n\n return c.json(\n {\n error: {\n code: \"internal_error\" as const,\n message: \"Something went wrong. Please try again.\",\n requestId: c.get(\"requestId\"),\n },\n },\n 500,\n );\n };\n}\n\n/** Coarse per-IP limit, applied to the whole API surface. */\nexport function rateLimit(): MiddlewareHandler<Env> {\n return async (c, next) => {\n const { limiters } = c.get(\"ctx\");\n const result = limiters.api.check(clientIp(c));\n\n if (!result.allowed) {\n throw new ApiError(\"rate_limited\", \"Too many requests. Please slow down.\", {\n headers: { \"retry-after\": String(result.retryAfter) },\n });\n }\n\n await next();\n };\n}\n\nconst SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\nconst normaliseOrigin = (value: string): string => {\n try {\n return new URL(value).origin;\n } catch {\n return value;\n }\n};\n\nexport function allowedOrigins(config: {\n MICA_PUBLIC_URL: string;\n adminOrigins: string[];\n}): string[] {\n return [...new Set([config.MICA_PUBLIC_URL, ...config.adminOrigins].map(normaliseOrigin))];\n}\n\n/**\n * First CSRF layer: the browser sets `Origin` on every state-changing request\n * and a page on another site cannot forge it.\n *\n * Applied to the whole API — including sign-in, which has no session yet and\n * so cannot use the token check below.\n */\nexport function originGuard(): MiddlewareHandler<Env> {\n return async (c, next) => {\n if (SAFE_METHODS.has(c.req.method)) {\n await next();\n return;\n }\n\n const { config } = c.get(\"ctx\");\n const origin = c.req.header(\"origin\");\n\n if (origin) {\n if (!allowedOrigins(config).includes(normaliseOrigin(origin))) {\n throw new ApiError(\"forbidden\", \"Request origin is not allowed.\");\n }\n } else if (config.isProduction) {\n // A browser always sends Origin on writes; its absence means the request\n // did not come from one. Tolerated in development for curl-based testing.\n throw new ApiError(\"forbidden\", \"Missing Origin header.\");\n }\n\n await next();\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-content.d.ts","sourceRoot":"","sources":["../../src/routes/admin-content.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"admin-content.d.ts","sourceRoot":"","sources":["../../src/routes/admin-content.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAmCtC,wBAAgB,kBAAkB,IAAI,IAAI,CAAC,GAAG,CAAC,CAoJ9C"}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* these routes only parse and serialize. The API path is the repository path,
|
|
4
4
|
* Git is the history, and `revision` makes concurrent edits safe.
|
|
5
5
|
*/
|
|
6
|
-
import { ValidationError } from "../services/content-store.js";
|
|
6
|
+
import { ReadOnlyError, ValidationError } from "../services/content-store.js";
|
|
7
7
|
import { ConflictError } from "../workspace/files.js";
|
|
8
|
-
import { collectionEntryPath, compareEntries, entryTitle, singletonPath } from "@micacms/core";
|
|
8
|
+
import { collectionEntryPath, compareEntries, entryTitle, LayoutError, singletonPath } from "@micacms/core";
|
|
9
9
|
import { Hono } from "hono";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { ApiError, badRequest, notFound } from "../lib/errors.js";
|
|
@@ -30,8 +30,9 @@ async function handled(run) {
|
|
|
30
30
|
return await run();
|
|
31
31
|
}
|
|
32
32
|
catch (error) {
|
|
33
|
-
if (error instanceof ConflictError)
|
|
33
|
+
if (error instanceof ConflictError || error instanceof ReadOnlyError || error instanceof LayoutError) {
|
|
34
34
|
throw new ApiError("conflict", error.message);
|
|
35
|
+
}
|
|
35
36
|
if (error instanceof ValidationError) {
|
|
36
37
|
throw new ApiError("validation_failed", error.message, { issues: error.issues });
|
|
37
38
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-content.js","sourceRoot":"","sources":["../../src/routes/admin-content.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Admin routes for entries and singletons. The content store holds every rule;\n * these routes only parse and serialize. The API path is the repository path,\n * Git is the history, and `revision` makes concurrent edits safe.\n */\n\nimport { ValidationError } from \"../services/content-store.js\";\nimport { ConflictError } from \"../workspace/files.js\";\nimport { collectionEntryPath, compareEntries, entryTitle, singletonPath } from \"@micacms/core\";\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport { ApiError, badRequest, notFound } from \"../lib/errors.js\";\nimport type { Env } from \"../http.js\";\n\nconst saveSchema = z.object({\n data: z.record(z.string(), z.unknown()),\n slug: z.string().max(120).optional(),\n /**\n * The revision the editor opened. `null` asserts that the entry does not\n * exist, so two parallel creates cannot overwrite.\n */\n revision: z.string().nullable().optional(),\n});\n\nconst reorderSchema = z.object({ slugs: z.array(z.string().min(1)).max(2000) });\n\nasync function jsonBody(c: { req: { json(): Promise<unknown> } }): Promise<unknown> {\n return c.req.json().catch(() => {\n throw badRequest(\"The request body must be valid JSON.\");\n });\n}\n\n/** Maps a conflict to 409 and a validation failure to 422; other errors go to the error handler. */\nasync function handled<T>(run: () => Promise<T>): Promise<T> {\n try {\n return await run();\n } catch (error) {\n if (error instanceof ConflictError) throw new ApiError(\"conflict\", error.message);\n if (error instanceof ValidationError) {\n throw new ApiError(\"validation_failed\", error.message, { issues: error.issues });\n }\n throw error;\n }\n}\n\nexport function adminContentRoutes(): Hono<Env> {\n const app = new Hono<Env>();\n\n app.get(\"/collections/:collection/entries\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const collection = c.req.param(\"collection\");\n const def = content.requireCollection(collection);\n\n const entries = await handled(() => content.list(collection));\n const search = c.req.query(\"search\")?.trim().toLowerCase();\n\n const items = entries\n .map((entry) => ({\n slug: entry.slug,\n position: entry.position,\n title: entryTitle(def, entry.data),\n data: entry.data,\n }))\n // Content trees are small, so an in-memory substring match is sufficient.\n .filter((item) => !search || JSON.stringify(item.data).toLowerCase().includes(search))\n .sort(compareEntries(def));\n\n return c.json({ items, total: items.length });\n });\n\n app.post(\"/collections/:collection/entries\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const parsed = saveSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"The entry payload is malformed.\");\n\n const saved = await handled(() =>\n content.save(c.req.param(\"collection\"), {\n data: parsed.data.data,\n expectedRevision: parsed.data.revision ?? null,\n }),\n );\n\n return c.json(saved, 201);\n });\n\n app.post(\"/collections/:collection/reorder\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const parsed = reorderSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"Expected a list of entry slugs.\");\n\n await handled(() => content.reorder(c.req.param(\"collection\"), parsed.data.slugs));\n return c.json({ ok: true });\n });\n\n /** Server-side slug generation, so the URL preview matches what is saved. */\n app.get(\"/collections/:collection/slug\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const source = c.req.query(\"source\") ?? \"\";\n if (!source.trim()) return c.json({ slug: \"\" });\n\n return c.json({\n slug: await handled(() =>\n content.suggestSlug(c.req.param(\"collection\"), source, c.req.query(\"except\")),\n ),\n });\n });\n\n app.get(\"/collections/:collection/entries/:slug\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const collection = c.req.param(\"collection\");\n const slug = c.req.param(\"slug\");\n\n const entry = await handled(() => content.get(collection, slug));\n if (!entry) throw notFound(\"This entry no longer exists.\");\n\n const def = content.requireCollection(collection);\n\n return c.json({\n collection,\n slug: entry.slug,\n position: entry.position,\n title: entryTitle(def, entry.data),\n data: entry.data,\n // From the model, so the editor need not know when an entry is a directory.\n path: collectionEntryPath(def.fields, collection, entry.slug),\n revision: await content.revisionOf(collection, slug),\n });\n });\n\n app.put(\"/collections/:collection/entries/:slug\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const parsed = saveSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"The entry payload is malformed.\");\n\n const saved = await handled(() =>\n content.save(c.req.param(\"collection\"), {\n data: parsed.data.data,\n slug: c.req.param(\"slug\"),\n expectedRevision: parsed.data.revision,\n }),\n );\n\n return c.json(saved);\n });\n\n app.delete(\"/collections/:collection/entries/:slug\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n\n await handled(() =>\n content.remove(c.req.param(\"collection\"), c.req.param(\"slug\")),\n );\n\n return c.json({ ok: true });\n });\n\n /** Entries elsewhere that point at this one — shown before a delete. */\n app.get(\"/collections/:collection/entries/:slug/references\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const references = await handled(() =>\n content.findReferences(c.req.param(\"collection\"), c.req.param(\"slug\")),\n );\n\n return c.json({ references });\n });\n\n app.get(\"/singletons/:name\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const name = c.req.param(\"name\");\n const def = content.requireSingleton(name);\n\n return c.json({\n name,\n label: def.label,\n path: singletonPath(def.fields, name),\n data: await handled(() => content.getSingleton(name)),\n revision: await content.revisionOfSingleton(name),\n });\n });\n\n app.put(\"/singletons/:name\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const body = (await jsonBody(c)) as { data?: unknown; revision?: string | null };\n\n const saved = await handled(() =>\n content.saveSingleton(c.req.param(\"name\"), body?.data ?? {}, {\n expectedRevision: body?.revision,\n }),\n );\n\n return c.json(saved);\n });\n\n return app;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"admin-content.js","sourceRoot":"","sources":["../../src/routes/admin-content.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Admin routes for entries and singletons. The content store holds every rule;\n * these routes only parse and serialize. The API path is the repository path,\n * Git is the history, and `revision` makes concurrent edits safe.\n */\n\nimport { ReadOnlyError, ValidationError } from \"../services/content-store.js\";\nimport { ConflictError } from \"../workspace/files.js\";\nimport { collectionEntryPath, compareEntries, entryTitle, LayoutError, singletonPath } from \"@micacms/core\";\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport { ApiError, badRequest, notFound } from \"../lib/errors.js\";\nimport type { Env } from \"../http.js\";\n\nconst saveSchema = z.object({\n data: z.record(z.string(), z.unknown()),\n slug: z.string().max(120).optional(),\n /**\n * The revision the editor opened. `null` asserts that the entry does not\n * exist, so two parallel creates cannot overwrite.\n */\n revision: z.string().nullable().optional(),\n});\n\nconst reorderSchema = z.object({ slugs: z.array(z.string().min(1)).max(2000) });\n\nasync function jsonBody(c: { req: { json(): Promise<unknown> } }): Promise<unknown> {\n return c.req.json().catch(() => {\n throw badRequest(\"The request body must be valid JSON.\");\n });\n}\n\n/** Maps a conflict to 409 and a validation failure to 422; other errors go to the error handler. */\nasync function handled<T>(run: () => Promise<T>): Promise<T> {\n try {\n return await run();\n } catch (error) {\n if (error instanceof ConflictError || error instanceof ReadOnlyError || error instanceof LayoutError) {\n throw new ApiError(\"conflict\", error.message);\n }\n if (error instanceof ValidationError) {\n throw new ApiError(\"validation_failed\", error.message, { issues: error.issues });\n }\n throw error;\n }\n}\n\nexport function adminContentRoutes(): Hono<Env> {\n const app = new Hono<Env>();\n\n app.get(\"/collections/:collection/entries\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const collection = c.req.param(\"collection\");\n const def = content.requireCollection(collection);\n\n const entries = await handled(() => content.list(collection));\n const search = c.req.query(\"search\")?.trim().toLowerCase();\n\n const items = entries\n .map((entry) => ({\n slug: entry.slug,\n position: entry.position,\n title: entryTitle(def, entry.data),\n data: entry.data,\n }))\n // Content trees are small, so an in-memory substring match is sufficient.\n .filter((item) => !search || JSON.stringify(item.data).toLowerCase().includes(search))\n .sort(compareEntries(def));\n\n return c.json({ items, total: items.length });\n });\n\n app.post(\"/collections/:collection/entries\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const parsed = saveSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"The entry payload is malformed.\");\n\n const saved = await handled(() =>\n content.save(c.req.param(\"collection\"), {\n data: parsed.data.data,\n expectedRevision: parsed.data.revision ?? null,\n }),\n );\n\n return c.json(saved, 201);\n });\n\n app.post(\"/collections/:collection/reorder\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const parsed = reorderSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"Expected a list of entry slugs.\");\n\n await handled(() => content.reorder(c.req.param(\"collection\"), parsed.data.slugs));\n return c.json({ ok: true });\n });\n\n /** Server-side slug generation, so the URL preview matches what is saved. */\n app.get(\"/collections/:collection/slug\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const source = c.req.query(\"source\") ?? \"\";\n if (!source.trim()) return c.json({ slug: \"\" });\n\n return c.json({\n slug: await handled(() =>\n content.suggestSlug(c.req.param(\"collection\"), source, c.req.query(\"except\")),\n ),\n });\n });\n\n app.get(\"/collections/:collection/entries/:slug\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const collection = c.req.param(\"collection\");\n const slug = c.req.param(\"slug\");\n\n const entry = await handled(() => content.get(collection, slug));\n if (!entry) throw notFound(\"This entry no longer exists.\");\n\n const def = content.requireCollection(collection);\n\n return c.json({\n collection,\n slug: entry.slug,\n position: entry.position,\n title: entryTitle(def, entry.data),\n data: entry.data,\n // From the model, so the editor need not know when an entry is a directory.\n path: collectionEntryPath(def.fields, collection, entry.slug),\n revision: await content.revisionOf(collection, slug),\n });\n });\n\n app.put(\"/collections/:collection/entries/:slug\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const parsed = saveSchema.safeParse(await jsonBody(c));\n if (!parsed.success) throw badRequest(\"The entry payload is malformed.\");\n\n const saved = await handled(() =>\n content.save(c.req.param(\"collection\"), {\n data: parsed.data.data,\n slug: c.req.param(\"slug\"),\n expectedRevision: parsed.data.revision,\n }),\n );\n\n return c.json(saved);\n });\n\n app.delete(\"/collections/:collection/entries/:slug\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n\n await handled(() =>\n content.remove(c.req.param(\"collection\"), c.req.param(\"slug\")),\n );\n\n return c.json({ ok: true });\n });\n\n /** Entries elsewhere that point at this one — shown before a delete. */\n app.get(\"/collections/:collection/entries/:slug/references\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const references = await handled(() =>\n content.findReferences(c.req.param(\"collection\"), c.req.param(\"slug\")),\n );\n\n return c.json({ references });\n });\n\n app.get(\"/singletons/:name\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const name = c.req.param(\"name\");\n const def = content.requireSingleton(name);\n\n return c.json({\n name,\n label: def.label,\n path: singletonPath(def.fields, name),\n data: await handled(() => content.getSingleton(name)),\n revision: await content.revisionOfSingleton(name),\n });\n });\n\n app.put(\"/singletons/:name\", async (c) => {\n const { content } = c.get(\"ctx\").services;\n const body = (await jsonBody(c)) as { data?: unknown; revision?: string | null };\n\n const saved = await handled(() =>\n content.saveSingleton(c.req.param(\"name\"), body?.data ?? {}, {\n expectedRevision: body?.revision,\n }),\n );\n\n return c.json(saved);\n });\n\n return app;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-media.d.ts","sourceRoot":"","sources":["../../src/routes/admin-media.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEtC,wBAAgB,gBAAgB,IAAI,IAAI,CAAC,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"admin-media.d.ts","sourceRoot":"","sources":["../../src/routes/admin-media.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEtC,wBAAgB,gBAAgB,IAAI,IAAI,CAAC,GAAG,CAAC,CAmF5C"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* An upload is written on its own, before the entry save, because the editor needs its path first.
|
|
4
4
|
*/
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import { badRequest, notFound } from "../lib/errors.js";
|
|
6
|
+
import { ApiError, badRequest, notFound } from "../lib/errors.js";
|
|
7
7
|
import { isImageType } from "../lib/file-type.js";
|
|
8
8
|
import { MAX_FILES_PER_UPLOAD } from "../middleware/index.js";
|
|
9
9
|
export function adminMediaRoutes() {
|
|
@@ -29,6 +29,11 @@ export function adminMediaRoutes() {
|
|
|
29
29
|
const accept = form.get("accept");
|
|
30
30
|
// An entry's own `media/` directory or the shared one; the media service refuses anything else.
|
|
31
31
|
const directory = typeof form.get("directory") === "string" ? String(form.get("directory")) : undefined;
|
|
32
|
+
// The media service accepts `/gallery/x/media` too; take the owner from the path it will write to.
|
|
33
|
+
const owner = (directory ?? "").replace(/^[/\\]+/, "").split(/[/\\]/)[0] ?? "";
|
|
34
|
+
const readOnly = ctx.site.readOnly.get(`collections.${owner}`) ?? ctx.site.readOnly.get(`singletons.${owner}`);
|
|
35
|
+
if (readOnly)
|
|
36
|
+
throw new ApiError("conflict", readOnly);
|
|
32
37
|
const prepared = await Promise.all(files.map((file) => ctx.services.media.prepare({
|
|
33
38
|
file,
|
|
34
39
|
directory,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-media.js","sourceRoot":"","sources":["../../src/routes/admin-media.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"admin-media.js","sourceRoot":"","sources":["../../src/routes/admin-media.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Admin media routes. Media is files in the content repository, and there is no delete endpoint.\n * An upload is written on its own, before the entry save, because the editor needs its path first.\n */\n\nimport { Hono } from \"hono\";\n\nimport { ApiError, badRequest, notFound } from \"../lib/errors.js\";\nimport { isImageType } from \"../lib/file-type.js\";\nimport { MAX_FILES_PER_UPLOAD } from \"../middleware/index.js\";\nimport type { Env } from \"../http.js\";\n\nexport function adminMediaRoutes(): Hono<Env> {\n const app = new Hono<Env>();\n\n app.get(\"/\", async (c) => {\n const { media } = c.get(\"ctx\").services;\n return c.json({ items: await media.list() });\n });\n\n app.post(\"/\", async (c) => {\n const ctx = c.get(\"ctx\");\n const form = await c.req.formData().catch(() => {\n throw badRequest(\"Expected a multipart upload.\");\n });\n\n // The cast picks one of the two structurally different `File` types (Node's and undici's).\n const files = form\n .getAll(\"file\")\n .filter((value) => typeof value !== \"string\") as unknown as File[];\n\n if (files.length === 0) throw badRequest(\"No file was uploaded.\");\n if (files.length > MAX_FILES_PER_UPLOAD) {\n throw badRequest(`Upload at most ${MAX_FILES_PER_UPLOAD} files at a time.`);\n }\n\n const accept = form.get(\"accept\");\n // An entry's own `media/` directory or the shared one; the media service refuses anything else.\n const directory = typeof form.get(\"directory\") === \"string\" ? String(form.get(\"directory\")) : undefined;\n // The media service accepts `/gallery/x/media` too; take the owner from the path it will write to.\n const owner = (directory ?? \"\").replace(/^[/\\\\]+/, \"\").split(/[/\\\\]/)[0] ?? \"\";\n const readOnly = ctx.site.readOnly.get(`collections.${owner}`) ?? ctx.site.readOnly.get(`singletons.${owner}`);\n if (readOnly) throw new ApiError(\"conflict\", readOnly);\n\n const prepared = await Promise.all(\n files.map((file) =>\n ctx.services.media.prepare({\n file,\n directory,\n accept: accept === \"image\" || accept === \"document\" ? accept : \"any\",\n }),\n ),\n );\n\n await ctx.site.contentRepository.apply({\n changes: prepared.map((item) => ({\n type: \"write\" as const,\n path: item.file.path,\n contents: item.contents,\n })),\n });\n\n return c.json({ items: prepared.map((item) => item.file) }, 201);\n });\n\n /**\n * Serves a file to the editor, behind the admin gate. The site build reads\n * the repository, not this route.\n */\n app.get(\"/file/*\", async (c) => {\n const { media } = c.get(\"ctx\").services;\n const mediaPath = c.req.path.replace(/^.*\\/file\\//, \"\");\n\n let decoded: string;\n try {\n decoded = decodeURIComponent(mediaPath);\n } catch {\n throw badRequest(\"The file path is not valid.\");\n }\n\n const found = await media.read(decoded);\n if (!found) throw notFound(\"No such file.\");\n\n return c.body(found.bytes as unknown as ArrayBuffer, 200, {\n // Text without a charset is decoded by guessing; the repository's text is UTF-8.\n \"content-type\": found.mimeType.startsWith(\"text/\") ? `${found.mimeType}; charset=utf-8` : found.mimeType,\n \"cache-control\": \"private, max-age=60\",\n // A file opened directly runs nothing and reaches nothing on this origin.\n \"content-security-policy\": \"sandbox; default-src 'none'\",\n // An image is shown as a preview; anything else is downloaded, never rendered here.\n ...(isImageType(found.mimeType) ? {} : { \"content-disposition\": \"attachment\" }),\n });\n });\n\n return app;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-system.d.ts","sourceRoot":"","sources":["../../src/routes/admin-system.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAMtC,wBAAgB,iBAAiB,IAAI,IAAI,CAAC,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"admin-system.d.ts","sourceRoot":"","sources":["../../src/routes/admin-system.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAMtC,wBAAgB,iBAAiB,IAAI,IAAI,CAAC,GAAG,CAAC,CAkI7C"}
|
|
@@ -25,12 +25,14 @@ export function adminSystemRoutes() {
|
|
|
25
25
|
slugField: resolveSlugField(def),
|
|
26
26
|
listedFields: listedFields(def),
|
|
27
27
|
fields: def.fields,
|
|
28
|
+
readOnly: site.readOnly.get(`collections.${def.name}`) ?? null,
|
|
28
29
|
})),
|
|
29
30
|
singletons: Object.values(site.model.singletons).map((def) => ({
|
|
30
31
|
name: def.name,
|
|
31
32
|
label: def.label,
|
|
32
33
|
description: def.description ?? null,
|
|
33
34
|
fields: def.fields,
|
|
35
|
+
readOnly: site.readOnly.get(`singletons.${def.name}`) ?? null,
|
|
34
36
|
})),
|
|
35
37
|
});
|
|
36
38
|
});
|
|
@@ -99,14 +101,14 @@ export function adminSystemRoutes() {
|
|
|
99
101
|
// A clone that Mica keeps names its remote in `repository.url` instead.
|
|
100
102
|
contentRoot: repository.owner === "developer" ? config.contentRoot : null,
|
|
101
103
|
dataDir: operational.dataDir ?? null,
|
|
102
|
-
logLevel: config.
|
|
103
|
-
maxUploadBytes: config.
|
|
104
|
-
maxUploadReadable: formatBytes(config.
|
|
104
|
+
logLevel: config.MICA_LOG_LEVEL,
|
|
105
|
+
maxUploadBytes: config.MICA_MAX_UPLOAD_BYTES,
|
|
106
|
+
maxUploadReadable: formatBytes(config.MICA_MAX_UPLOAD_BYTES),
|
|
105
107
|
sessionTtlHours: operational.sessionTtlHours ?? null,
|
|
106
108
|
sessionIdleHours: operational.sessionIdleHours ?? null,
|
|
107
109
|
auth: operational.auth,
|
|
108
110
|
admission: operational.admission ?? null,
|
|
109
|
-
trustProxy: config.
|
|
111
|
+
trustProxy: config.MICA_TRUST_PROXY,
|
|
110
112
|
adminOrigins: config.adminOrigins,
|
|
111
113
|
generatedSecrets: operational.generatedSecrets ?? [],
|
|
112
114
|
memoryBytes: process.memoryUsage().rss,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-system.js","sourceRoot":"","sources":["../../src/routes/admin-system.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Schema, dashboard and system information. The SPA knows no collection; it\n * renders what `/schema` returns.\n */\n\nimport { Hono } from \"hono\";\n\nimport { listedFields, resolveSlugField, resolveTitleField } from \"@micacms/core\";\n\nimport type { Env } from \"../http.js\";\nimport { schemaStats } from \"../lib/schema-stats.js\";\nimport { schemaOrigin } from \"../schema.js\";\nimport { VERSION } from \"../lib/version.js\";\nimport { formatBytes } from \"../services/media.js\";\n\nexport function adminSystemRoutes(): Hono<Env> {\n const app = new Hono<Env>();\n\n /** The content model, plus the derived values that the UI would otherwise recompute. */\n app.get(\"/schema\", (c) => {\n const { site } = c.get(\"ctx\");\n\n return c.json({\n collections: Object.values(site.model.collections).map((def) => ({\n name: def.name,\n label: def.label,\n labelSingular: def.labelSingular ?? def.label,\n description: def.description ?? null,\n sortable: def.sortable ?? false,\n defaultSort: def.defaultSort ?? null,\n titleField: resolveTitleField(def),\n slugField: resolveSlugField(def),\n listedFields: listedFields(def),\n fields: def.fields,\n })),\n singletons: Object.values(site.model.singletons).map((def) => ({\n name: def.name,\n label: def.label,\n description: def.description ?? null,\n fields: def.fields,\n })),\n });\n });\n\n app.get(\"/overview\", async (c) => {\n const ctx = c.get(\"ctx\");\n const { content, media } = ctx.services;\n\n const collections = await Promise.all(\n Object.values(ctx.site.model.collections).map(async (def) => ({\n name: def.name,\n label: def.label,\n total: (await content.list(def.name)).length,\n })),\n );\n\n const files = await media.list();\n\n return c.json({\n collections,\n media: {\n files: files.length,\n bytes: files.reduce((sum, file) => sum + file.size, 0),\n readable: formatBytes(files.reduce((sum, file) => sum + file.size, 0)),\n },\n repository: ctx.site.contentRepository.origin,\n });\n });\n\n /**\n * What this deployment is and serves. Every admitted editor sees it, because\n * there is no administrator role. It never returns a secret value.\n */\n app.get(\"/system\", async (c) => {\n const ctx = c.get(\"ctx\");\n const { config } = ctx;\n\n const stats = schemaStats(ctx.site.model);\n\n const counts = Object.fromEntries(\n await Promise.all(\n Object.values(ctx.site.model.collections).map(\n async (def) => [def.name, (await ctx.services.content.list(def.name)).length] as const,\n ),\n ),\n );\n\n const files = await ctx.services.media.list();\n const mediaBytes = files.reduce((sum, file) => sum + file.size, 0);\n\n const uptimeSeconds = Math.floor(process.uptime());\n\n const operational = ctx.operational();\n const repository = await ctx.services.staging.repository();\n\n return c.json({\n cms: {\n version: VERSION,\n environment: config.NODE_ENV,\n node: process.version,\n platform: `${process.platform}/${process.arch}`,\n startedAt: new Date(Date.now() - uptimeSeconds * 1000).toISOString(),\n uptimeSeconds,\n },\n schema: {\n totals: stats.totals,\n kinds: stats.kinds,\n relations: stats.relations,\n collections: stats.collections.map((item) => ({\n ...item,\n entries: counts[item.name] ?? 0,\n })),\n singletons: stats.singletons,\n },\n repository: {\n origin: ctx.site.contentRepository.origin,\n entries: Object.values(counts).reduce((sum, count) => sum + count, 0),\n media: { files: files.length, bytes: mediaBytes, readable: formatBytes(mediaBytes) },\n ...repository,\n },\n deployment: {\n site: ctx.site.id,\n baseUrl: config.MICA_PUBLIC_URL,\n schemaOrigin: schemaOrigin(ctx.site.contentRepository),\n // A clone that Mica keeps names its remote in `repository.url` instead.\n contentRoot: repository.owner === \"developer\" ? config.contentRoot : null,\n dataDir: operational.dataDir ?? null,\n logLevel: config.LOG_LEVEL,\n maxUploadBytes: config.MAX_UPLOAD_BYTES,\n maxUploadReadable: formatBytes(config.MAX_UPLOAD_BYTES),\n sessionTtlHours: operational.sessionTtlHours ?? null,\n sessionIdleHours: operational.sessionIdleHours ?? null,\n auth: operational.auth,\n admission: operational.admission ?? null,\n trustProxy: config.TRUST_PROXY,\n adminOrigins: config.adminOrigins,\n generatedSecrets: operational.generatedSecrets ?? [],\n memoryBytes: process.memoryUsage().rss,\n },\n });\n });\n\n return app;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"admin-system.js","sourceRoot":"","sources":["../../src/routes/admin-system.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlF,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,UAAU,iBAAiB;IAC/B,MAAM,GAAG,GAAG,IAAI,IAAI,EAAO,CAAC;IAE5B,wFAAwF;IACxF,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;QACvB,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAE9B,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAC/D,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,KAAK,EAAE,GAAG,CAAC,KAAK;gBAChB,aAAa,EAAE,GAAG,CAAC,aAAa,IAAI,GAAG,CAAC,KAAK;gBAC7C,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;gBACpC,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,KAAK;gBAC/B,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;gBACpC,UAAU,EAAE,iBAAiB,CAAC,GAAG,CAAC;gBAClC,SAAS,EAAE,gBAAgB,CAAC,GAAG,CAAC;gBAChC,YAAY,EAAE,YAAY,CAAC,GAAG,CAAC;gBAC/B,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,eAAe,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI;aAC/D,CAAC,CAAC;YACH,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAC7D,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,KAAK,EAAE,GAAG,CAAC,KAAK;gBAChB,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;gBACpC,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAc,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI;aAC9D,CAAC,CAAC;SACJ,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC/B,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACzB,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC,QAAQ,CAAC;QAExC,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,GAAG,CACnC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;YAC5D,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,KAAK,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM;SAC7C,CAAC,CAAC,CACJ,CAAC;QAEF,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;QAEjC,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,WAAW;YACX,KAAK,EAAE;gBACL,KAAK,EAAE,KAAK,CAAC,MAAM;gBACnB,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;gBACtD,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;aACvE;YACD,UAAU,EAAE,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM;SAC9C,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH;;;OAGG;IACH,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACzB,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;QAEvB,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE1C,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAC/B,MAAM,OAAO,CAAC,GAAG,CACf,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,GAAG,CAC3C,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAU,CACvF,CACF,CACF,CAAC;QAEF,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAC9C,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAEnE,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAEnD,MAAM,WAAW,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QACtC,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;QAE3D,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,GAAG,EAAE;gBACH,OAAO,EAAE,OAAO;gBAChB,WAAW,EAAE,MAAM,CAAC,QAAQ;gBAC5B,IAAI,EAAE,OAAO,CAAC,OAAO;gBACrB,QAAQ,EAAE,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE;gBAC/C,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,aAAa,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;gBACpE,aAAa;aACd;YACD,MAAM,EAAE;gBACN,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;oBAC5C,GAAG,IAAI;oBACP,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;iBAChC,CAAC,CAAC;gBACH,UAAU,EAAE,KAAK,CAAC,UAAU;aAC7B;YACD,UAAU,EAAE;gBACV,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM;gBACzC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC;gBACrE,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,CAAC,UAAU,CAAC,EAAE;gBACpF,GAAG,UAAU;aACd;YACD,UAAU,EAAE;gBACV,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE;gBACjB,OAAO,EAAE,MAAM,CAAC,eAAe;gBAC/B,YAAY,EAAE,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC;gBACtD,wEAAwE;gBACxE,WAAW,EAAE,UAAU,CAAC,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI;gBACzE,OAAO,EAAE,WAAW,CAAC,OAAO,IAAI,IAAI;gBACpC,QAAQ,EAAE,MAAM,CAAC,cAAc;gBAC/B,cAAc,EAAE,MAAM,CAAC,qBAAqB;gBAC5C,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,qBAAqB,CAAC;gBAC5D,eAAe,EAAE,WAAW,CAAC,eAAe,IAAI,IAAI;gBACpD,gBAAgB,EAAE,WAAW,CAAC,gBAAgB,IAAI,IAAI;gBACtD,IAAI,EAAE,WAAW,CAAC,IAAI;gBACtB,SAAS,EAAE,WAAW,CAAC,SAAS,IAAI,IAAI;gBACxC,UAAU,EAAE,MAAM,CAAC,gBAAgB;gBACnC,YAAY,EAAE,MAAM,CAAC,YAAY;gBACjC,gBAAgB,EAAE,WAAW,CAAC,gBAAgB,IAAI,EAAE;gBACpD,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,GAAG;aACvC;SACF,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * Schema, dashboard and system information. The SPA knows no collection; it\n * renders what `/schema` returns.\n */\n\nimport { Hono } from \"hono\";\n\nimport { listedFields, resolveSlugField, resolveTitleField } from \"@micacms/core\";\n\nimport type { Env } from \"../http.js\";\nimport { schemaStats } from \"../lib/schema-stats.js\";\nimport { schemaOrigin } from \"../schema.js\";\nimport { VERSION } from \"../lib/version.js\";\nimport { formatBytes } from \"../services/media.js\";\n\nexport function adminSystemRoutes(): Hono<Env> {\n const app = new Hono<Env>();\n\n /** The content model, plus the derived values that the UI would otherwise recompute. */\n app.get(\"/schema\", (c) => {\n const { site } = c.get(\"ctx\");\n\n return c.json({\n collections: Object.values(site.model.collections).map((def) => ({\n name: def.name,\n label: def.label,\n labelSingular: def.labelSingular ?? def.label,\n description: def.description ?? null,\n sortable: def.sortable ?? false,\n defaultSort: def.defaultSort ?? null,\n titleField: resolveTitleField(def),\n slugField: resolveSlugField(def),\n listedFields: listedFields(def),\n fields: def.fields,\n readOnly: site.readOnly.get(`collections.${def.name}`) ?? null,\n })),\n singletons: Object.values(site.model.singletons).map((def) => ({\n name: def.name,\n label: def.label,\n description: def.description ?? null,\n fields: def.fields,\n readOnly: site.readOnly.get(`singletons.${def.name}`) ?? null,\n })),\n });\n });\n\n app.get(\"/overview\", async (c) => {\n const ctx = c.get(\"ctx\");\n const { content, media } = ctx.services;\n\n const collections = await Promise.all(\n Object.values(ctx.site.model.collections).map(async (def) => ({\n name: def.name,\n label: def.label,\n total: (await content.list(def.name)).length,\n })),\n );\n\n const files = await media.list();\n\n return c.json({\n collections,\n media: {\n files: files.length,\n bytes: files.reduce((sum, file) => sum + file.size, 0),\n readable: formatBytes(files.reduce((sum, file) => sum + file.size, 0)),\n },\n repository: ctx.site.contentRepository.origin,\n });\n });\n\n /**\n * What this deployment is and serves. Every admitted editor sees it, because\n * there is no administrator role. It never returns a secret value.\n */\n app.get(\"/system\", async (c) => {\n const ctx = c.get(\"ctx\");\n const { config } = ctx;\n\n const stats = schemaStats(ctx.site.model);\n\n const counts = Object.fromEntries(\n await Promise.all(\n Object.values(ctx.site.model.collections).map(\n async (def) => [def.name, (await ctx.services.content.list(def.name)).length] as const,\n ),\n ),\n );\n\n const files = await ctx.services.media.list();\n const mediaBytes = files.reduce((sum, file) => sum + file.size, 0);\n\n const uptimeSeconds = Math.floor(process.uptime());\n\n const operational = ctx.operational();\n const repository = await ctx.services.staging.repository();\n\n return c.json({\n cms: {\n version: VERSION,\n environment: config.NODE_ENV,\n node: process.version,\n platform: `${process.platform}/${process.arch}`,\n startedAt: new Date(Date.now() - uptimeSeconds * 1000).toISOString(),\n uptimeSeconds,\n },\n schema: {\n totals: stats.totals,\n kinds: stats.kinds,\n relations: stats.relations,\n collections: stats.collections.map((item) => ({\n ...item,\n entries: counts[item.name] ?? 0,\n })),\n singletons: stats.singletons,\n },\n repository: {\n origin: ctx.site.contentRepository.origin,\n entries: Object.values(counts).reduce((sum, count) => sum + count, 0),\n media: { files: files.length, bytes: mediaBytes, readable: formatBytes(mediaBytes) },\n ...repository,\n },\n deployment: {\n site: ctx.site.id,\n baseUrl: config.MICA_PUBLIC_URL,\n schemaOrigin: schemaOrigin(ctx.site.contentRepository),\n // A clone that Mica keeps names its remote in `repository.url` instead.\n contentRoot: repository.owner === \"developer\" ? config.contentRoot : null,\n dataDir: operational.dataDir ?? null,\n logLevel: config.MICA_LOG_LEVEL,\n maxUploadBytes: config.MICA_MAX_UPLOAD_BYTES,\n maxUploadReadable: formatBytes(config.MICA_MAX_UPLOAD_BYTES),\n sessionTtlHours: operational.sessionTtlHours ?? null,\n sessionIdleHours: operational.sessionIdleHours ?? null,\n auth: operational.auth,\n admission: operational.admission ?? null,\n trustProxy: config.MICA_TRUST_PROXY,\n adminOrigins: config.adminOrigins,\n generatedSecrets: operational.generatedSecrets ?? [],\n memoryBytes: process.memoryUsage().rss,\n },\n });\n });\n\n return app;\n}\n"]}
|
package/dist/schema.d.ts
CHANGED
|
@@ -15,11 +15,23 @@ export declare function schemaOrigin(source: ContentSource): string;
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function loadSchema(source: ContentSource): Promise<Site>;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* One message for each collection or singleton with entries in the other
|
|
19
|
+
* layout. Mica refuses to start on any: editing next to hidden entries would
|
|
20
|
+
* let a save create a second copy of a page.
|
|
21
|
+
*/
|
|
22
|
+
export declare function layoutProblems(source: ContentSource, site: Site): Promise<string[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Runs the `defineSite` checks on untrusted input. A newer `protocolVersion` is
|
|
25
|
+
* refused; an unknown field kind is not, and `readOnlyParts` names what it locks.
|
|
21
26
|
*/
|
|
22
27
|
export declare function checkSchema(raw: unknown, origin?: string): Site;
|
|
28
|
+
/**
|
|
29
|
+
* The collections and singletons this build of Mica must not write, keyed
|
|
30
|
+
* `collections.<name>` or `singletons.<name>`, with the reason for the editor.
|
|
31
|
+
* A field kind from a newer @micacms/astro makes only its own collection or
|
|
32
|
+
* singleton read-only; Mica still starts, and the rest stays editable.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readOnlyParts(site: Site): Map<string, string>;
|
|
23
35
|
/**
|
|
24
36
|
* A short digest of a model, to show drift. Field order counts, because it is
|
|
25
37
|
* the order of the admin form.
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAaL,KAAK,aAAa,EAGlB,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAEvB,qBAAa,WAAY,SAAQ,KAAK;IAC3B,IAAI,SAAiB;CAC/B;AAED,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAE1D;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BrE;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAWzF;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,SAAe,GAAG,IAAI,CAuDrE;AAmBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAsB7D;AAUD;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAGnE"}
|