@micacms/runtime 0.6.0-rc.2 → 0.7.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.
Files changed (70) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +5 -0
  3. package/dist/app.js.map +1 -1
  4. package/dist/context.d.ts +17 -2
  5. package/dist/context.d.ts.map +1 -1
  6. package/dist/context.js +62 -18
  7. package/dist/context.js.map +1 -1
  8. package/dist/http.d.ts +2 -0
  9. package/dist/http.d.ts.map +1 -1
  10. package/dist/http.js +5 -0
  11. package/dist/http.js.map +1 -1
  12. package/dist/lib/file-type.d.ts +2 -0
  13. package/dist/lib/file-type.d.ts.map +1 -1
  14. package/dist/lib/file-type.js +3 -1
  15. package/dist/lib/file-type.js.map +1 -1
  16. package/dist/routes/admin-content.d.ts +1 -1
  17. package/dist/routes/admin-content.d.ts.map +1 -1
  18. package/dist/routes/admin-content.js +15 -0
  19. package/dist/routes/admin-content.js.map +1 -1
  20. package/dist/routes/admin-media.d.ts +5 -3
  21. package/dist/routes/admin-media.d.ts.map +1 -1
  22. package/dist/routes/admin-media.js +72 -2
  23. package/dist/routes/admin-media.js.map +1 -1
  24. package/dist/routes/admin-staging.d.ts.map +1 -1
  25. package/dist/routes/admin-staging.js +18 -7
  26. package/dist/routes/admin-staging.js.map +1 -1
  27. package/dist/routes/admin-system.d.ts.map +1 -1
  28. package/dist/routes/admin-system.js +4 -0
  29. package/dist/routes/admin-system.js.map +1 -1
  30. package/dist/services/changes.d.ts +19 -4
  31. package/dist/services/changes.d.ts.map +1 -1
  32. package/dist/services/changes.js +60 -1
  33. package/dist/services/changes.js.map +1 -1
  34. package/dist/services/content-store.d.ts +27 -6
  35. package/dist/services/content-store.d.ts.map +1 -1
  36. package/dist/services/content-store.js +231 -18
  37. package/dist/services/content-store.js.map +1 -1
  38. package/dist/services/media.d.ts.map +1 -1
  39. package/dist/services/media.js +4 -1
  40. package/dist/services/media.js.map +1 -1
  41. package/dist/services/staging.d.ts.map +1 -1
  42. package/dist/services/staging.js +26 -14
  43. package/dist/services/staging.js.map +1 -1
  44. package/dist/workspace/developer.d.ts +2 -1
  45. package/dist/workspace/developer.d.ts.map +1 -1
  46. package/dist/workspace/developer.js +19 -4
  47. package/dist/workspace/developer.js.map +1 -1
  48. package/dist/workspace/policy.d.ts +1 -1
  49. package/dist/workspace/policy.d.ts.map +1 -1
  50. package/dist/workspace/policy.js.map +1 -1
  51. package/dist/workspace/tree.d.ts +15 -2
  52. package/dist/workspace/tree.d.ts.map +1 -1
  53. package/dist/workspace/tree.js +29 -6
  54. package/dist/workspace/tree.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/app.ts +5 -0
  57. package/src/context.ts +69 -20
  58. package/src/http.ts +6 -0
  59. package/src/lib/file-type.ts +4 -1
  60. package/src/routes/admin-content.ts +16 -2
  61. package/src/routes/admin-media.ts +74 -3
  62. package/src/routes/admin-staging.ts +22 -7
  63. package/src/routes/admin-system.ts +4 -0
  64. package/src/services/changes.ts +84 -5
  65. package/src/services/content-store.ts +243 -25
  66. package/src/services/media.ts +7 -1
  67. package/src/services/staging.ts +30 -14
  68. package/src/workspace/developer.ts +22 -5
  69. package/src/workspace/policy.ts +1 -1
  70. package/src/workspace/tree.ts +35 -6
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAiBrC,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IAClB,wFAAwF;IACxF,IAAI,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,QAAQ,IAAI,iBAAiB,CAAC,GAAG,CAAC,CAKjD;AASD,MAAM,WAAW,gBAAgB;IAC/B,sDAAsD;IACtD,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,GAAE,gBAAqB,GAAG,IAAI,CAAC,GAAG,CAAC,CA6DxF"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAiBrC,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IAClB,wFAAwF;IACxF,IAAI,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,QAAQ,IAAI,iBAAiB,CAAC,GAAG,CAAC,CAKjD;AASD,MAAM,WAAW,gBAAgB;IAC/B,sDAAsD;IACtD,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,GAAE,gBAAqB,GAAG,IAAI,CAAC,GAAG,CAAC,CAkExF"}
package/dist/app.js CHANGED
@@ -53,6 +53,11 @@ export function createApp(ctx, options = {}) {
53
53
  // One gate and no roles: whoever the surface admits is an editor of this site.
54
54
  for (const middleware of auth.gate)
55
55
  app.use("/api/admin/*", middleware);
56
+ // A model pushed or written since is used at once; editors cannot restart the server.
57
+ app.use("/api/admin/*", async (c, next) => {
58
+ await c.get("ctx").refreshModel();
59
+ await next();
60
+ });
56
61
  app.route("/api/admin", adminSystemRoutes());
57
62
  app.route("/api/admin", adminContentRoutes());
58
63
  app.route("/api/admin", adminStagingRoutes());
package/dist/app.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Assembles the app: health, the admin API and the admin SPA. There is no\n * public content API and no `/media/*`; a website builds from the repository,\n * not from this server. Authentication comes from the composing app.\n */\n\nimport { Hono } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport type { MiddlewareHandler } from \"hono\";\n\nimport type { RuntimeContext } from \"./context.js\";\nimport type { Env } from \"./http.js\";\nimport {\n allowedOrigins,\n errorHandler,\n originGuard,\n requestSizeLimit,\n rateLimit,\n requestContext,\n securityHeaders,\n} from \"./middleware/index.js\";\nimport { adminContentRoutes } from \"./routes/admin-content.js\";\nimport { adminMediaRoutes } from \"./routes/admin-media.js\";\nimport { adminSpaRoutes, adminAssetsExist } from \"./routes/admin-spa.js\";\nimport { adminStagingRoutes } from \"./routes/admin-staging.js\";\nimport { adminSystemRoutes } from \"./routes/admin-system.js\";\nimport { healthRoutes } from \"./routes/health.js\";\n\n/** What a composing app contributes around the auth-neutral runtime. */\nexport interface AuthSurface {\n /** Mounted at `/api/auth`: sign-in, callback, logout, whatever auth needs. */\n routes: Hono<Env>;\n /** Applied before every `/api/admin` route. Must set `access` or refuse the request. */\n gate: MiddlewareHandler<Env>[];\n}\n\n/**\n * Allows every request with no identity. Safe only because the local editor is\n * loopback-bound and Host-guarded.\n */\nexport function allowAll(): MiddlewareHandler<Env> {\n return async (c, next) => {\n c.set(\"access\", { identity: null });\n await next();\n };\n}\n\n/** `/api/auth/config` with nothing behind it: the SPA's cue to skip sign-in. */\nfunction unauthenticatedSurface(): AuthSurface {\n const routes = new Hono<Env>();\n routes.get(\"/config\", (c) => c.json({ authRequired: false, provider: null }));\n return { routes, gate: [allowAll()] };\n}\n\nexport interface CreateAppOptions {\n /** Absent means the unauthenticated local surface. */\n auth?: AuthSurface;\n /**\n * Middleware in front of everything, so the local editor's Host allowlist\n * also guards the health endpoints.\n */\n guards?: MiddlewareHandler<Env>[];\n adminDir?: string;\n}\n\nexport function createApp(ctx: RuntimeContext, options: CreateAppOptions = {}): Hono<Env> {\n const app = new Hono<Env>();\n const auth = options.auth ?? unauthenticatedSurface();\n\n app.use(\"*\", requestContext(ctx));\n for (const guard of options.guards ?? []) app.use(\"*\", guard);\n app.use(\"*\", securityHeaders());\n app.onError(errorHandler());\n\n app.route(\"/\", healthRoutes());\n\n // Only for the Vite dev server on another origin; in production the SPA is same-origin.\n if (ctx.config.adminOrigins.length > 0) {\n app.use(\n \"/api/*\",\n cors({\n origin: allowedOrigins(ctx.config),\n credentials: true,\n allowHeaders: [\"content-type\", \"x-csrf-token\"],\n allowMethods: [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\"],\n }),\n );\n }\n\n app.use(\"/api/*\", rateLimit());\n app.use(\"/api/*\", originGuard());\n app.use(\"/api/*\", requestSizeLimit());\n\n app.route(\"/api/auth\", auth.routes);\n\n // One gate and no roles: whoever the surface admits is an editor of this site.\n for (const middleware of auth.gate) app.use(\"/api/admin/*\", middleware);\n\n app.route(\"/api/admin\", adminSystemRoutes());\n app.route(\"/api/admin\", adminContentRoutes());\n app.route(\"/api/admin\", adminStagingRoutes());\n app.route(\"/api/admin/media\", adminMediaRoutes());\n\n app.all(\"/api/*\", (c) =>\n c.json({ error: { code: \"not_found\" as const, message: \"No such endpoint.\" } }, 404),\n );\n\n const adminDir = options.adminDir ?? ctx.config.MICA_ADMIN_DIR;\n\n if (adminDir && adminAssetsExist(adminDir)) {\n app.route(\"/admin\", adminSpaRoutes(adminDir));\n } else {\n app.get(\"/admin/*\", (c) =>\n c.text(\n \"The admin interface has not been built.\\n\" +\n \"Run `rush build`, or use `rush dev` and open the Vite dev server.\",\n 503,\n ),\n );\n }\n\n app.get(\"/\", (c) => c.redirect(\"/admin/\"));\n\n app.notFound((c) => c.text(\"Not found\", 404));\n\n return app;\n}\n"]}
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Assembles the app: health, the admin API and the admin SPA. There is no\n * public content API and no `/media/*`; a website builds from the repository,\n * not from this server. Authentication comes from the composing app.\n */\n\nimport { Hono } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport type { MiddlewareHandler } from \"hono\";\n\nimport type { RuntimeContext } from \"./context.js\";\nimport type { Env } from \"./http.js\";\nimport {\n allowedOrigins,\n errorHandler,\n originGuard,\n requestSizeLimit,\n rateLimit,\n requestContext,\n securityHeaders,\n} from \"./middleware/index.js\";\nimport { adminContentRoutes } from \"./routes/admin-content.js\";\nimport { adminMediaRoutes } from \"./routes/admin-media.js\";\nimport { adminSpaRoutes, adminAssetsExist } from \"./routes/admin-spa.js\";\nimport { adminStagingRoutes } from \"./routes/admin-staging.js\";\nimport { adminSystemRoutes } from \"./routes/admin-system.js\";\nimport { healthRoutes } from \"./routes/health.js\";\n\n/** What a composing app contributes around the auth-neutral runtime. */\nexport interface AuthSurface {\n /** Mounted at `/api/auth`: sign-in, callback, logout, whatever auth needs. */\n routes: Hono<Env>;\n /** Applied before every `/api/admin` route. Must set `access` or refuse the request. */\n gate: MiddlewareHandler<Env>[];\n}\n\n/**\n * Allows every request with no identity. Safe only because the local editor is\n * loopback-bound and Host-guarded.\n */\nexport function allowAll(): MiddlewareHandler<Env> {\n return async (c, next) => {\n c.set(\"access\", { identity: null });\n await next();\n };\n}\n\n/** `/api/auth/config` with nothing behind it: the SPA's cue to skip sign-in. */\nfunction unauthenticatedSurface(): AuthSurface {\n const routes = new Hono<Env>();\n routes.get(\"/config\", (c) => c.json({ authRequired: false, provider: null }));\n return { routes, gate: [allowAll()] };\n}\n\nexport interface CreateAppOptions {\n /** Absent means the unauthenticated local surface. */\n auth?: AuthSurface;\n /**\n * Middleware in front of everything, so the local editor's Host allowlist\n * also guards the health endpoints.\n */\n guards?: MiddlewareHandler<Env>[];\n adminDir?: string;\n}\n\nexport function createApp(ctx: RuntimeContext, options: CreateAppOptions = {}): Hono<Env> {\n const app = new Hono<Env>();\n const auth = options.auth ?? unauthenticatedSurface();\n\n app.use(\"*\", requestContext(ctx));\n for (const guard of options.guards ?? []) app.use(\"*\", guard);\n app.use(\"*\", securityHeaders());\n app.onError(errorHandler());\n\n app.route(\"/\", healthRoutes());\n\n // Only for the Vite dev server on another origin; in production the SPA is same-origin.\n if (ctx.config.adminOrigins.length > 0) {\n app.use(\n \"/api/*\",\n cors({\n origin: allowedOrigins(ctx.config),\n credentials: true,\n allowHeaders: [\"content-type\", \"x-csrf-token\"],\n allowMethods: [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\"],\n }),\n );\n }\n\n app.use(\"/api/*\", rateLimit());\n app.use(\"/api/*\", originGuard());\n app.use(\"/api/*\", requestSizeLimit());\n\n app.route(\"/api/auth\", auth.routes);\n\n // One gate and no roles: whoever the surface admits is an editor of this site.\n for (const middleware of auth.gate) app.use(\"/api/admin/*\", middleware);\n // A model pushed or written since is used at once; editors cannot restart the server.\n app.use(\"/api/admin/*\", async (c, next) => {\n await c.get(\"ctx\").refreshModel();\n await next();\n });\n\n app.route(\"/api/admin\", adminSystemRoutes());\n app.route(\"/api/admin\", adminContentRoutes());\n app.route(\"/api/admin\", adminStagingRoutes());\n app.route(\"/api/admin/media\", adminMediaRoutes());\n\n app.all(\"/api/*\", (c) =>\n c.json({ error: { code: \"not_found\" as const, message: \"No such endpoint.\" } }, 404),\n );\n\n const adminDir = options.adminDir ?? ctx.config.MICA_ADMIN_DIR;\n\n if (adminDir && adminAssetsExist(adminDir)) {\n app.route(\"/admin\", adminSpaRoutes(adminDir));\n } else {\n app.get(\"/admin/*\", (c) =>\n c.text(\n \"The admin interface has not been built.\\n\" +\n \"Run `rush build`, or use `rush dev` and open the Vite dev server.\",\n 503,\n ),\n );\n }\n\n app.get(\"/\", (c) => c.redirect(\"/admin/\"));\n\n app.notFound((c) => c.text(\"Not found\", 404));\n\n return app;\n}\n"]}
package/dist/context.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * The app supplies the working tree's files and the owner policy, so the
4
4
  * runtime stays authentication-neutral.
5
5
  */
6
- import type { Site } from "@micacms/core";
6
+ import { type Site } from "@micacms/core";
7
7
  import type { RuntimeConfig } from "./config.js";
8
8
  import { type Logger } from "./lib/logger.js";
9
9
  import { type RateLimiter } from "./lib/rate-limit.js";
@@ -18,7 +18,7 @@ import type { GitTree } from "./workspace/tree.js";
18
18
  export interface SiteContext {
19
19
  /** Identifies this site in logs. */
20
20
  id: string;
21
- /** The content model, read from the repository at startup. */
21
+ /** The content model in use: read at startup, and again when `.mica/schema.json` changes. */
22
22
  model: Site;
23
23
  /** The working tree's files behind the workspace locks. Only Publish makes a commit. */
24
24
  contentRepository: ContentFiles;
@@ -29,6 +29,11 @@ export interface SiteContext {
29
29
  schemaRevision: string | null;
30
30
  /** Collections and singletons this build cannot write, from `readOnlyParts`, with the reason. */
31
31
  readOnly: ReadonlyMap<string, string>;
32
+ /** Why the newer model in the working tree is not in use. The model above stays in use. */
33
+ modelWarning: {
34
+ message: string;
35
+ revision: string | null;
36
+ } | null;
32
37
  }
33
38
  export declare const DEFAULT_SITE_ID = "default";
34
39
  /** Facts about the composition that the System page shows and the runtime cannot know. */
@@ -68,6 +73,14 @@ export interface RuntimeContext {
68
73
  };
69
74
  operational(): OperationalInfo;
70
75
  health: RuntimeHealth;
76
+ /**
77
+ * Uses a new content model when `.mica/schema.json` changed, and keeps the one
78
+ * in use when the new one cannot be read. Checks at most every two seconds,
79
+ * unless `force`.
80
+ */
81
+ refreshModel(options?: {
82
+ force?: boolean;
83
+ }): Promise<void>;
71
84
  shutdown(): Promise<void>;
72
85
  }
73
86
  export interface CreateRuntimeContextOptions {
@@ -87,6 +100,8 @@ export interface CreateRuntimeContextOptions {
87
100
  locks?: WorkspaceLocks;
88
101
  /** See `WorkingTreeStagingDeps.tree`. */
89
102
  tree?: GitTree;
103
+ /** Called after a new model is in use, for what the app keeps beside the runtime. */
104
+ onModel?: (model: Site) => void;
90
105
  onShutdown?: () => void | Promise<void>;
91
106
  }
92
107
  export declare function createRuntimeContext(options: CreateRuntimeContextOptions): RuntimeContext;
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAE1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAE1E,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEpF,OAAO,EAAqC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC9F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,8DAA8D;IAC9D,KAAK,EAAE,IAAI,CAAC;IACZ,wFAAwF;IACxF,iBAAiB,EAAE,YAAY,CAAC;IAChC;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iGAAiG;IACjG,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,eAAe,YAAY,CAAC;AAEzC,0FAA0F;AAC1F,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,wCAAwC;AACxC,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CAClD;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE;QACR,OAAO,EAAE,YAAY,CAAC;QACtB,KAAK,EAAE,YAAY,CAAC;QACpB,OAAO,EAAE,qBAAqB,CAAC;KAChC,CAAC;IACF,QAAQ,EAAE;QACR,GAAG,EAAE,WAAW,CAAC;KAClB,CAAC;IACF,WAAW,IAAI,eAAe,CAAC;IAC/B,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,IAAI,CAAC;IACZ,kEAAkE;IAClE,KAAK,EAAE,YAAY,CAAC;IACpB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,eAAe,CAAC;IACpC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,oEAAoE;IACpE,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,yCAAyC;IACzC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,cAAc,CAmEzF"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAiB,KAAK,IAAI,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAE1E,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEpF,OAAO,EAAqC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC9F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,6FAA6F;IAC7F,KAAK,EAAE,IAAI,CAAC;IACZ,wFAAwF;IACxF,iBAAiB,EAAE,YAAY,CAAC;IAChC;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iGAAiG;IACjG,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,2FAA2F;IAC3F,YAAY,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CACnE;AAED,eAAO,MAAM,eAAe,YAAY,CAAC;AAEzC,0FAA0F;AAC1F,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,wCAAwC;AACxC,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CAClD;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE;QACR,OAAO,EAAE,YAAY,CAAC;QACtB,KAAK,EAAE,YAAY,CAAC;QACpB,OAAO,EAAE,qBAAqB,CAAC;KAChC,CAAC;IACF,QAAQ,EAAE;QACR,GAAG,EAAE,WAAW,CAAC;KAClB,CAAC;IACF,WAAW,IAAI,eAAe,CAAC;IAC/B,MAAM,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,IAAI,CAAC;IACZ,kEAAkE;IAClE,KAAK,EAAE,YAAY,CAAC;IACpB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,eAAe,CAAC;IACpC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,oEAAoE;IACpE,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,yCAAyC;IACzC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,qFAAqF;IACrF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,KAAK,IAAI,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,cAAc,CA0GzF"}
package/dist/context.js CHANGED
@@ -3,9 +3,10 @@
3
3
  * The app supplies the working tree's files and the owner policy, so the
4
4
  * runtime stays authentication-neutral.
5
5
  */
6
+ import { MANIFEST_PATH } from "@micacms/core";
6
7
  import { createLogger } from "./lib/logger.js";
7
8
  import { createRateLimiter } from "./lib/rate-limit.js";
8
- import { readOnlyParts } from "./schema.js";
9
+ import { loadSchema, readOnlyParts, SchemaError } from "./schema.js";
9
10
  import { createMediaService } from "./services/media.js";
10
11
  import { createContentStore } from "./services/content-store.js";
11
12
  import { createWorkingTreeStagingService } from "./services/staging.js";
@@ -21,34 +22,76 @@ export function createRuntimeContext(options) {
21
22
  const schemaRevision = options.schemaRevision ?? null;
22
23
  // Services write through the lock wrapper; staging takes the locks itself.
23
24
  const locks = options.locks ?? createWorkspaceLocks();
24
- const readOnly = readOnlyParts(model);
25
- for (const reason of readOnly.values())
26
- logger.warn("Read-only in this version of Mica", { reason });
27
25
  const site = {
28
26
  id: options.siteId ?? DEFAULT_SITE_ID,
29
27
  model,
30
28
  contentRepository: lockedFiles(options.files, locks.tree),
31
29
  schemaRevision,
32
- readOnly,
30
+ readOnly: new Map(),
31
+ modelWarning: null,
33
32
  };
33
+ /** The services that read the model. Media does not. */
34
+ const forModel = (current, revision) => {
35
+ const readOnly = readOnlyParts(current);
36
+ for (const reason of readOnly.values())
37
+ logger.warn("Read-only in this version of Mica", { reason });
38
+ site.model = current;
39
+ site.schemaRevision = revision;
40
+ site.readOnly = readOnly;
41
+ return {
42
+ content: createContentStore(current, site.contentRepository, readOnly),
43
+ staging: createWorkingTreeStagingService({
44
+ model: current,
45
+ files: options.files,
46
+ contentRoot: config.contentRoot,
47
+ schemaRevision: revision,
48
+ readOnly,
49
+ publishEnabled: config.MICA_LOCAL_PUBLISH,
50
+ logger,
51
+ locks,
52
+ ...(options.policy ? { policy: options.policy } : {}),
53
+ ...(options.tree ? { tree: options.tree } : {}),
54
+ }),
55
+ };
56
+ };
57
+ // Routes read `ctx.services` on each request, so a reload replaces these in place.
34
58
  const services = {
35
- content: createContentStore(model, site.contentRepository, readOnly),
59
+ ...forModel(model, schemaRevision),
36
60
  media: createMediaService({
37
61
  files: site.contentRepository,
38
62
  maxBytes: config.MICA_MAX_UPLOAD_BYTES,
39
63
  }),
40
- staging: createWorkingTreeStagingService({
41
- model,
42
- files: options.files,
43
- contentRoot: config.contentRoot,
44
- schemaRevision,
45
- readOnly,
46
- publishEnabled: config.MICA_LOCAL_PUBLISH,
47
- logger,
48
- locks,
49
- ...(options.policy ? { policy: options.policy } : {}),
50
- ...(options.tree ? { tree: options.tree } : {}),
51
- }),
64
+ };
65
+ let checkedAt = 0;
66
+ let checking = null;
67
+ const refreshModel = (refresh = {}) => {
68
+ // A forced check must read the files as they are now, not join one that started earlier.
69
+ if (checking)
70
+ return refresh.force ? checking.then(() => refreshModel(refresh)) : checking;
71
+ if (!refresh.force && Date.now() - checkedAt < 2_000)
72
+ return Promise.resolve();
73
+ checking = (async () => {
74
+ checkedAt = Date.now();
75
+ const revision = await options.files.revision(MANIFEST_PATH);
76
+ if (revision === site.schemaRevision || revision === site.modelWarning?.revision)
77
+ return;
78
+ try {
79
+ const next = await loadSchema(options.files);
80
+ Object.assign(services, forModel(next, revision));
81
+ site.modelWarning = null;
82
+ logger.info("Using the new content model", { revision });
83
+ options.onModel?.(next);
84
+ }
85
+ catch (error) {
86
+ if (!(error instanceof SchemaError))
87
+ throw error;
88
+ site.modelWarning = { message: error.message, revision };
89
+ logger.warn("The new content model cannot be read; the one in use stays", { message: error.message });
90
+ }
91
+ })().finally(() => {
92
+ checking = null;
93
+ });
94
+ return checking;
52
95
  };
53
96
  const limiters = {
54
97
  // Generous: it stops runaway scripts, it does not shape traffic.
@@ -63,6 +106,7 @@ export function createRuntimeContext(options) {
63
106
  operational: options.operational ??
64
107
  (() => ({ auth: "unspecified" })),
65
108
  health: options.health ?? {},
109
+ refreshModel,
66
110
  async shutdown() {
67
111
  limiters.api.stop();
68
112
  await options.onShutdown?.();
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Builds the runtime's services for one site, with no module-level singletons.\n * The app supplies the working tree's files and the owner policy, so the\n * runtime stays authentication-neutral.\n */\n\nimport type { Site } from \"@micacms/core\";\n\nimport type { RuntimeConfig } from \"./config.js\";\nimport { createLogger, type Logger } from \"./lib/logger.js\";\nimport { createRateLimiter, type RateLimiter } from \"./lib/rate-limit.js\";\nimport { readOnlyParts } from \"./schema.js\";\nimport { createMediaService, type MediaService } from \"./services/media.js\";\nimport type { ContentStagingService } from \"./services/changes.js\";\nimport { createContentStore, type ContentStore } from \"./services/content-store.js\";\nimport { createWorkingTreeStagingService } from \"./services/staging.js\";\nimport { createWorkspaceLocks, lockedFiles, type WorkspaceLocks } from \"./workspace/locks.js\";\nimport type { OwnerPolicy } from \"./workspace/policy.js\";\nimport type { ContentFiles } from \"./workspace/files.js\";\nimport type { GitTree } from \"./workspace/tree.js\";\n\n/** The one site this process serves. Several sites means several processes. */\nexport interface SiteContext {\n /** Identifies this site in logs. */\n id: string;\n /** The content model, read from the repository at startup. */\n model: Site;\n /** The working tree's files behind the workspace locks. Only Publish makes a commit. */\n contentRepository: ContentFiles;\n /**\n * Revision of `.mica/schema.json` when the model was loaded. The publish\n * drift gate compares against it.\n */\n schemaRevision: string | null;\n /** Collections and singletons this build cannot write, from `readOnlyParts`, with the reason. */\n readOnly: ReadonlyMap<string, string>;\n}\n\nexport const DEFAULT_SITE_ID = \"default\";\n\n/** Facts about the composition that the System page shows and the runtime cannot know. */\nexport interface OperationalInfo {\n /** How this deployment authenticates, in words. */\n auth: string;\n /** What decides who may edit, in the operator's own words. */\n admission?: string;\n dataDir?: string;\n sessionTtlHours?: number;\n sessionIdleHours?: number;\n /** Names of the secrets this process generated for itself. */\n generatedSecrets?: string[];\n}\n\n/** What /ready asks the composition. */\nexport interface RuntimeHealth {\n /**\n * Whether the site can be edited, with short reason codes. Never names the\n * repository: the endpoint is public.\n */\n ready?: () => { ok: boolean; reasons: string[] };\n}\n\nexport interface RuntimeContext {\n config: RuntimeConfig;\n site: SiteContext;\n logger: Logger;\n services: {\n content: ContentStore;\n media: MediaService;\n staging: ContentStagingService;\n };\n limiters: {\n api: RateLimiter;\n };\n operational(): OperationalInfo;\n health: RuntimeHealth;\n shutdown(): Promise<void>;\n}\n\nexport interface CreateRuntimeContextOptions {\n config: RuntimeConfig;\n model: Site;\n /** The working tree's files, without locks. The app builds it. */\n files: ContentFiles;\n /** Revision of the manifest the model was read from. Null when unknown. */\n schemaRevision?: string | null;\n siteId?: string;\n logger?: Logger;\n operational?: () => OperationalInfo;\n health?: RuntimeHealth;\n /** Who owns the working tree. Defaults to the developer. */\n policy?: OwnerPolicy;\n /** Shared with the owner's own work on the tree, such as a sync. */\n locks?: WorkspaceLocks;\n /** See `WorkingTreeStagingDeps.tree`. */\n tree?: GitTree;\n onShutdown?: () => void | Promise<void>;\n}\n\nexport function createRuntimeContext(options: CreateRuntimeContextOptions): RuntimeContext {\n const { config, model } = options;\n\n const logger =\n options.logger ??\n createLogger({\n level: config.MICA_LOG_LEVEL,\n pretty: !config.isProduction,\n });\n\n const schemaRevision = options.schemaRevision ?? null;\n\n // Services write through the lock wrapper; staging takes the locks itself.\n const locks = options.locks ?? createWorkspaceLocks();\n\n const readOnly = readOnlyParts(model);\n for (const reason of readOnly.values()) logger.warn(\"Read-only in this version of Mica\", { reason });\n\n const site: SiteContext = {\n id: options.siteId ?? DEFAULT_SITE_ID,\n model,\n contentRepository: lockedFiles(options.files, locks.tree),\n schemaRevision,\n readOnly,\n };\n\n const services = {\n content: createContentStore(model, site.contentRepository, readOnly),\n media: createMediaService({\n files: site.contentRepository,\n maxBytes: config.MICA_MAX_UPLOAD_BYTES,\n }),\n staging: createWorkingTreeStagingService({\n model,\n files: options.files,\n contentRoot: config.contentRoot,\n schemaRevision,\n readOnly,\n publishEnabled: config.MICA_LOCAL_PUBLISH,\n logger,\n locks,\n ...(options.policy ? { policy: options.policy } : {}),\n ...(options.tree ? { tree: options.tree } : {}),\n }),\n };\n\n const limiters = {\n // Generous: it stops runaway scripts, it does not shape traffic.\n api: createRateLimiter({ limit: 600, windowMs: 60_000 }),\n };\n\n return {\n config,\n site,\n logger,\n services,\n limiters,\n operational:\n options.operational ??\n (() => ({ auth: \"unspecified\" })),\n health: options.health ?? {},\n\n async shutdown() {\n limiters.api.stop();\n await options.onShutdown?.();\n },\n };\n}\n"]}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Builds the runtime's services for one site, with no module-level singletons.\n * The app supplies the working tree's files and the owner policy, so the\n * runtime stays authentication-neutral.\n */\n\nimport { MANIFEST_PATH, type Site } from \"@micacms/core\";\n\nimport type { RuntimeConfig } from \"./config.js\";\nimport { createLogger, type Logger } from \"./lib/logger.js\";\nimport { createRateLimiter, type RateLimiter } from \"./lib/rate-limit.js\";\nimport { loadSchema, readOnlyParts, SchemaError } from \"./schema.js\";\nimport { createMediaService, type MediaService } from \"./services/media.js\";\nimport type { ContentStagingService } from \"./services/changes.js\";\nimport { createContentStore, type ContentStore } from \"./services/content-store.js\";\nimport { createWorkingTreeStagingService } from \"./services/staging.js\";\nimport { createWorkspaceLocks, lockedFiles, type WorkspaceLocks } from \"./workspace/locks.js\";\nimport type { OwnerPolicy } from \"./workspace/policy.js\";\nimport type { ContentFiles } from \"./workspace/files.js\";\nimport type { GitTree } from \"./workspace/tree.js\";\n\n/** The one site this process serves. Several sites means several processes. */\nexport interface SiteContext {\n /** Identifies this site in logs. */\n id: string;\n /** The content model in use: read at startup, and again when `.mica/schema.json` changes. */\n model: Site;\n /** The working tree's files behind the workspace locks. Only Publish makes a commit. */\n contentRepository: ContentFiles;\n /**\n * Revision of `.mica/schema.json` when the model was loaded. The publish\n * drift gate compares against it.\n */\n schemaRevision: string | null;\n /** Collections and singletons this build cannot write, from `readOnlyParts`, with the reason. */\n readOnly: ReadonlyMap<string, string>;\n /** Why the newer model in the working tree is not in use. The model above stays in use. */\n modelWarning: { message: string; revision: string | null } | null;\n}\n\nexport const DEFAULT_SITE_ID = \"default\";\n\n/** Facts about the composition that the System page shows and the runtime cannot know. */\nexport interface OperationalInfo {\n /** How this deployment authenticates, in words. */\n auth: string;\n /** What decides who may edit, in the operator's own words. */\n admission?: string;\n dataDir?: string;\n sessionTtlHours?: number;\n sessionIdleHours?: number;\n /** Names of the secrets this process generated for itself. */\n generatedSecrets?: string[];\n}\n\n/** What /ready asks the composition. */\nexport interface RuntimeHealth {\n /**\n * Whether the site can be edited, with short reason codes. Never names the\n * repository: the endpoint is public.\n */\n ready?: () => { ok: boolean; reasons: string[] };\n}\n\nexport interface RuntimeContext {\n config: RuntimeConfig;\n site: SiteContext;\n logger: Logger;\n services: {\n content: ContentStore;\n media: MediaService;\n staging: ContentStagingService;\n };\n limiters: {\n api: RateLimiter;\n };\n operational(): OperationalInfo;\n health: RuntimeHealth;\n /**\n * Uses a new content model when `.mica/schema.json` changed, and keeps the one\n * in use when the new one cannot be read. Checks at most every two seconds,\n * unless `force`.\n */\n refreshModel(options?: { force?: boolean }): Promise<void>;\n shutdown(): Promise<void>;\n}\n\nexport interface CreateRuntimeContextOptions {\n config: RuntimeConfig;\n model: Site;\n /** The working tree's files, without locks. The app builds it. */\n files: ContentFiles;\n /** Revision of the manifest the model was read from. Null when unknown. */\n schemaRevision?: string | null;\n siteId?: string;\n logger?: Logger;\n operational?: () => OperationalInfo;\n health?: RuntimeHealth;\n /** Who owns the working tree. Defaults to the developer. */\n policy?: OwnerPolicy;\n /** Shared with the owner's own work on the tree, such as a sync. */\n locks?: WorkspaceLocks;\n /** See `WorkingTreeStagingDeps.tree`. */\n tree?: GitTree;\n /** Called after a new model is in use, for what the app keeps beside the runtime. */\n onModel?: (model: Site) => void;\n onShutdown?: () => void | Promise<void>;\n}\n\nexport function createRuntimeContext(options: CreateRuntimeContextOptions): RuntimeContext {\n const { config, model } = options;\n\n const logger =\n options.logger ??\n createLogger({\n level: config.MICA_LOG_LEVEL,\n pretty: !config.isProduction,\n });\n\n const schemaRevision = options.schemaRevision ?? null;\n\n // Services write through the lock wrapper; staging takes the locks itself.\n const locks = options.locks ?? createWorkspaceLocks();\n\n const site: SiteContext = {\n id: options.siteId ?? DEFAULT_SITE_ID,\n model,\n contentRepository: lockedFiles(options.files, locks.tree),\n schemaRevision,\n readOnly: new Map(),\n modelWarning: null,\n };\n\n /** The services that read the model. Media does not. */\n const forModel = (current: Site, revision: string | null) => {\n const readOnly = readOnlyParts(current);\n for (const reason of readOnly.values()) logger.warn(\"Read-only in this version of Mica\", { reason });\n site.model = current;\n site.schemaRevision = revision;\n site.readOnly = readOnly;\n return {\n content: createContentStore(current, site.contentRepository, readOnly),\n staging: createWorkingTreeStagingService({\n model: current,\n files: options.files,\n contentRoot: config.contentRoot,\n schemaRevision: revision,\n readOnly,\n publishEnabled: config.MICA_LOCAL_PUBLISH,\n logger,\n locks,\n ...(options.policy ? { policy: options.policy } : {}),\n ...(options.tree ? { tree: options.tree } : {}),\n }),\n };\n };\n\n // Routes read `ctx.services` on each request, so a reload replaces these in place.\n const services = {\n ...forModel(model, schemaRevision),\n media: createMediaService({\n files: site.contentRepository,\n maxBytes: config.MICA_MAX_UPLOAD_BYTES,\n }),\n };\n\n let checkedAt = 0;\n let checking: Promise<void> | null = null;\n const refreshModel = (refresh: { force?: boolean } = {}): Promise<void> => {\n // A forced check must read the files as they are now, not join one that started earlier.\n if (checking) return refresh.force ? checking.then(() => refreshModel(refresh)) : checking;\n if (!refresh.force && Date.now() - checkedAt < 2_000) return Promise.resolve();\n checking = (async () => {\n checkedAt = Date.now();\n const revision = await options.files.revision(MANIFEST_PATH);\n if (revision === site.schemaRevision || revision === site.modelWarning?.revision) return;\n try {\n const next = await loadSchema(options.files);\n Object.assign(services, forModel(next, revision));\n site.modelWarning = null;\n logger.info(\"Using the new content model\", { revision });\n options.onModel?.(next);\n } catch (error) {\n if (!(error instanceof SchemaError)) throw error;\n site.modelWarning = { message: error.message, revision };\n logger.warn(\"The new content model cannot be read; the one in use stays\", { message: error.message });\n }\n })().finally(() => {\n checking = null;\n });\n return checking;\n };\n\n const limiters = {\n // Generous: it stops runaway scripts, it does not shape traffic.\n api: createRateLimiter({ limit: 600, windowMs: 60_000 }),\n };\n\n return {\n config,\n site,\n logger,\n services,\n limiters,\n operational:\n options.operational ??\n (() => ({ auth: \"unspecified\" })),\n health: options.health ?? {},\n refreshModel,\n\n async shutdown() {\n limiters.api.stop();\n await options.onShutdown?.();\n },\n };\n}\n"]}
package/dist/http.d.ts CHANGED
@@ -29,4 +29,6 @@ export declare function actorOf(c: Context<Env>): {
29
29
  email: string;
30
30
  name: string;
31
31
  } | null;
32
+ /** Who did it, for the log: the signed-in editor's email, else their subject. None in the local editor. */
33
+ export declare function editorOf(c: Context<Env>): string | undefined;
32
34
  //# sourceMappingURL=http.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,MAAM,WAAW,GAAG;IAClB,SAAS,EAAE;QACT,GAAG,EAAE,cAAc,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB;;;WAGG;QACH,MAAM,EAAE,aAAa,CAAC;KACvB,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAWhD;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAG3F"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,MAAM,WAAW,GAAG;IAClB,SAAS,EAAE;QACT,GAAG,EAAE,cAAc,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB;;;WAGG;QACH,MAAM,EAAE,aAAa,CAAC;KACvB,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAWhD;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAG3F;AAED,2GAA2G;AAC3G,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,SAAS,CAG5D"}
package/dist/http.js CHANGED
@@ -22,4 +22,9 @@ export function actorOf(c) {
22
22
  const identity = c.get("access")?.identity;
23
23
  return identity ? { id: identity.subject, email: identity.email, name: identity.name } : null;
24
24
  }
25
+ /** Who did it, for the log: the signed-in editor's email, else their subject. None in the local editor. */
26
+ export function editorOf(c) {
27
+ const actor = actorOf(c);
28
+ return actor ? actor.email || actor.id : undefined;
29
+ }
25
30
  //# sourceMappingURL=http.js.map
package/dist/http.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAoBA;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,CAAe;IACtC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAEhC,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QAClD,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;IACxB,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,CAAC,GAAyE,CAAC;IACzF,OAAO,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,IAAI,SAAS,CAAC;AAC5D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,CAAe;IACrC,MAAM,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC3C,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAChG,CAAC","sourcesContent":["import type { Context } from \"hono\";\n\nimport type { AccessContext } from \"@micacms/core\";\n\nimport type { RuntimeContext } from \"./context.js\";\nimport type { Logger } from \"./lib/logger.js\";\n\nexport interface Env {\n Variables: {\n ctx: RuntimeContext;\n logger: Logger;\n requestId: string;\n /**\n * Who this request acts as. The auth surface's gate sets it on every admin\n * route; the runtime never sets it.\n */\n access: AccessContext;\n };\n}\n\n/**\n * Client IP. Reads `X-Forwarded-For` only with MICA_TRUST_PROXY, or a client could\n * spoof its address past the rate limit. The right-most entry is the address\n * the proxy saw; the entries to its left come from the client.\n */\nexport function clientIp(c: Context<Env>): string {\n const { config } = c.get(\"ctx\");\n\n if (config.MICA_TRUST_PROXY) {\n const forwarded = c.req.header(\"x-forwarded-for\");\n const last = forwarded?.split(\",\").at(-1)?.trim();\n if (last) return last;\n }\n\n const info = c.env as { incoming?: { socket?: { remoteAddress?: string } } } | undefined;\n return info?.incoming?.socket?.remoteAddress ?? \"unknown\";\n}\n\n/**\n * The commit author. Null in the local editor, where the commit keeps the\n * developer's own Git identity.\n */\nexport function actorOf(c: Context<Env>): { id: string; email: string; name: string } | null {\n const identity = c.get(\"access\")?.identity;\n return identity ? { id: identity.subject, email: identity.email, name: identity.name } : null;\n}\n"]}
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAoBA;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,CAAe;IACtC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAEhC,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QAClD,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;IACxB,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,CAAC,GAAyE,CAAC;IACzF,OAAO,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,IAAI,SAAS,CAAC;AAC5D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,CAAe;IACrC,MAAM,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC3C,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAChG,CAAC;AAED,2GAA2G;AAC3G,MAAM,UAAU,QAAQ,CAAC,CAAe;IACtC,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACrD,CAAC","sourcesContent":["import type { Context } from \"hono\";\n\nimport type { AccessContext } from \"@micacms/core\";\n\nimport type { RuntimeContext } from \"./context.js\";\nimport type { Logger } from \"./lib/logger.js\";\n\nexport interface Env {\n Variables: {\n ctx: RuntimeContext;\n logger: Logger;\n requestId: string;\n /**\n * Who this request acts as. The auth surface's gate sets it on every admin\n * route; the runtime never sets it.\n */\n access: AccessContext;\n };\n}\n\n/**\n * Client IP. Reads `X-Forwarded-For` only with MICA_TRUST_PROXY, or a client could\n * spoof its address past the rate limit. The right-most entry is the address\n * the proxy saw; the entries to its left come from the client.\n */\nexport function clientIp(c: Context<Env>): string {\n const { config } = c.get(\"ctx\");\n\n if (config.MICA_TRUST_PROXY) {\n const forwarded = c.req.header(\"x-forwarded-for\");\n const last = forwarded?.split(\",\").at(-1)?.trim();\n if (last) return last;\n }\n\n const info = c.env as { incoming?: { socket?: { remoteAddress?: string } } } | undefined;\n return info?.incoming?.socket?.remoteAddress ?? \"unknown\";\n}\n\n/**\n * The commit author. Null in the local editor, where the commit keeps the\n * developer's own Git identity.\n */\nexport function actorOf(c: Context<Env>): { id: string; email: string; name: string } | null {\n const identity = c.get(\"access\")?.identity;\n return identity ? { id: identity.subject, email: identity.email, name: identity.name } : null;\n}\n\n/** Who did it, for the log: the signed-in editor's email, else their subject. None in the local editor. */\nexport function editorOf(c: Context<Env>): string | undefined {\n const actor = actorOf(c);\n return actor ? actor.email || actor.id : undefined;\n}\n"]}
@@ -18,6 +18,8 @@ export declare const isImageType: (mimeType: string) => boolean;
18
18
  * be stored at all.
19
19
  */
20
20
  export declare function isAccepted(mimeType: string, accept: "image" | "document" | "any"): boolean;
21
+ /** A video is large for a Git repository, and the media list cannot show one. */
22
+ export declare const VIDEO_TYPE = "video/mp4";
21
23
  /**
22
24
  * SVG can carry scripts, so Mica neither stores nor shows it (#86). It is still
23
25
  * detected, so that the refusal can say why.
@@ -1 +1 @@
1
- {"version":3,"file":"file-type.d.ts","sourceRoot":"","sources":["../../src/lib/file-type.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAQD,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,YAAY,GAAG,IAAI,CA2CrE;AAqBD,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,KAAG,OAAoC,CAAC;AAEpF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,KAAK,GAAG,OAAO,CAI1F;AAED;;;GAGG;AACH,eAAO,MAAM,QAAQ,kBAAkB,CAAC"}
1
+ {"version":3,"file":"file-type.d.ts","sourceRoot":"","sources":["../../src/lib/file-type.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAQD,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,YAAY,GAAG,IAAI,CA2CrE;AAqBD,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,KAAG,OAAoC,CAAC;AAEpF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,KAAK,GAAG,OAAO,CAI1F;AAED,iFAAiF;AACjF,eAAO,MAAM,UAAU,cAAc,CAAC;AAEtC;;;GAGG;AACH,eAAO,MAAM,QAAQ,kBAAkB,CAAC"}
@@ -72,8 +72,10 @@ export function isAccepted(mimeType, accept) {
72
72
  return IMAGE_TYPES.has(mimeType);
73
73
  if (accept === "document")
74
74
  return DOCUMENT_TYPES.has(mimeType);
75
- return IMAGE_TYPES.has(mimeType) || DOCUMENT_TYPES.has(mimeType) || mimeType === "video/mp4";
75
+ return IMAGE_TYPES.has(mimeType) || DOCUMENT_TYPES.has(mimeType);
76
76
  }
77
+ /** A video is large for a Git repository, and the media list cannot show one. */
78
+ export const VIDEO_TYPE = "video/mp4";
77
79
  /**
78
80
  * SVG can carry scripts, so Mica neither stores nor shows it (#86). It is still
79
81
  * detected, so that the refusal can say why.
@@ -1 +1 @@
1
- {"version":3,"file":"file-type.js","sourceRoot":"","sources":["../../src/lib/file-type.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Content sniffing for uploads.\n *\n * The browser-supplied MIME type is attacker-controlled and must not be\n * trusted: an executable renamed to `.jpg` arrives as `image/jpeg`. The stored\n * type is derived from the bytes instead, and the extension is regenerated\n * from it rather than taken from the filename.\n */\n\nexport interface DetectedType {\n mimeType: string;\n extension: string;\n}\n\nconst startsWith = (bytes: Uint8Array, signature: number[], offset = 0): boolean =>\n signature.every((byte, index) => bytes[offset + index] === byte);\n\nconst ascii = (bytes: Uint8Array, offset: number, length: number): string =>\n Array.from(bytes.slice(offset, offset + length), (byte) => String.fromCharCode(byte)).join(\"\");\n\nexport function detectFileType(bytes: Uint8Array): DetectedType | null {\n if (bytes.length < 12) return null;\n\n if (startsWith(bytes, [0xff, 0xd8, 0xff])) {\n return { mimeType: \"image/jpeg\", extension: \"jpg\" };\n }\n\n if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {\n return { mimeType: \"image/png\", extension: \"png\" };\n }\n\n const header = ascii(bytes, 0, 6);\n if (header === \"GIF87a\" || header === \"GIF89a\") {\n return { mimeType: \"image/gif\", extension: \"gif\" };\n }\n\n if (ascii(bytes, 0, 4) === \"RIFF\" && ascii(bytes, 8, 4) === \"WEBP\") {\n return { mimeType: \"image/webp\", extension: \"webp\" };\n }\n\n // ISO base media formats declare their brand at offset 8, after the box size.\n if (ascii(bytes, 4, 4) === \"ftyp\") {\n const brand = ascii(bytes, 8, 4);\n if (brand === \"avif\" || brand === \"avis\") {\n return { mimeType: \"image/avif\", extension: \"avif\" };\n }\n if (brand === \"heic\" || brand === \"heix\" || brand === \"mif1\") {\n return { mimeType: \"image/heic\", extension: \"heic\" };\n }\n if (brand === \"isom\" || brand === \"mp42\" || brand === \"M4V \") {\n return { mimeType: \"video/mp4\", extension: \"mp4\" };\n }\n }\n\n if (ascii(bytes, 0, 5) === \"%PDF-\") {\n return { mimeType: \"application/pdf\", extension: \"pdf\" };\n }\n\n if (isSvg(bytes)) {\n return { mimeType: \"image/svg+xml\", extension: \"svg\" };\n }\n\n return null;\n}\n\n/** SVG has no magic number, so look for a root `<svg>` in the leading text. */\nfunction isSvg(bytes: Uint8Array): boolean {\n const head = new TextDecoder(\"utf-8\", { fatal: false })\n .decode(bytes.slice(0, 1024))\n .trimStart()\n .toLowerCase();\n\n if (head.startsWith(\"<svg\")) return true;\n if (head.startsWith(\"<?xml\") || head.startsWith(\"<!doctype svg\")) {\n return head.includes(\"<svg\");\n }\n return false;\n}\n\n// No HEIC: Astro's image service cannot read it, so a site could not show it.\nconst IMAGE_TYPES = new Set([\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\", \"image/avif\"]);\n\nconst DOCUMENT_TYPES = new Set([\"application/pdf\"]);\n\nexport const isImageType = (mimeType: string): boolean => IMAGE_TYPES.has(mimeType);\n\n/**\n * Whether a detected type is acceptable for a field's `accept` setting.\n * `any` deliberately does not mean \"anything\": only the types listed here can\n * be stored at all.\n */\nexport function isAccepted(mimeType: string, accept: \"image\" | \"document\" | \"any\"): boolean {\n if (accept === \"image\") return IMAGE_TYPES.has(mimeType);\n if (accept === \"document\") return DOCUMENT_TYPES.has(mimeType);\n return IMAGE_TYPES.has(mimeType) || DOCUMENT_TYPES.has(mimeType) || mimeType === \"video/mp4\";\n}\n\n/**\n * SVG can carry scripts, so Mica neither stores nor shows it (#86). It is still\n * detected, so that the refusal can say why.\n */\nexport const SVG_TYPE = \"image/svg+xml\";\n"]}
1
+ {"version":3,"file":"file-type.js","sourceRoot":"","sources":["../../src/lib/file-type.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Content sniffing for uploads.\n *\n * The browser-supplied MIME type is attacker-controlled and must not be\n * trusted: an executable renamed to `.jpg` arrives as `image/jpeg`. The stored\n * type is derived from the bytes instead, and the extension is regenerated\n * from it rather than taken from the filename.\n */\n\nexport interface DetectedType {\n mimeType: string;\n extension: string;\n}\n\nconst startsWith = (bytes: Uint8Array, signature: number[], offset = 0): boolean =>\n signature.every((byte, index) => bytes[offset + index] === byte);\n\nconst ascii = (bytes: Uint8Array, offset: number, length: number): string =>\n Array.from(bytes.slice(offset, offset + length), (byte) => String.fromCharCode(byte)).join(\"\");\n\nexport function detectFileType(bytes: Uint8Array): DetectedType | null {\n if (bytes.length < 12) return null;\n\n if (startsWith(bytes, [0xff, 0xd8, 0xff])) {\n return { mimeType: \"image/jpeg\", extension: \"jpg\" };\n }\n\n if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {\n return { mimeType: \"image/png\", extension: \"png\" };\n }\n\n const header = ascii(bytes, 0, 6);\n if (header === \"GIF87a\" || header === \"GIF89a\") {\n return { mimeType: \"image/gif\", extension: \"gif\" };\n }\n\n if (ascii(bytes, 0, 4) === \"RIFF\" && ascii(bytes, 8, 4) === \"WEBP\") {\n return { mimeType: \"image/webp\", extension: \"webp\" };\n }\n\n // ISO base media formats declare their brand at offset 8, after the box size.\n if (ascii(bytes, 4, 4) === \"ftyp\") {\n const brand = ascii(bytes, 8, 4);\n if (brand === \"avif\" || brand === \"avis\") {\n return { mimeType: \"image/avif\", extension: \"avif\" };\n }\n if (brand === \"heic\" || brand === \"heix\" || brand === \"mif1\") {\n return { mimeType: \"image/heic\", extension: \"heic\" };\n }\n if (brand === \"isom\" || brand === \"mp42\" || brand === \"M4V \") {\n return { mimeType: \"video/mp4\", extension: \"mp4\" };\n }\n }\n\n if (ascii(bytes, 0, 5) === \"%PDF-\") {\n return { mimeType: \"application/pdf\", extension: \"pdf\" };\n }\n\n if (isSvg(bytes)) {\n return { mimeType: \"image/svg+xml\", extension: \"svg\" };\n }\n\n return null;\n}\n\n/** SVG has no magic number, so look for a root `<svg>` in the leading text. */\nfunction isSvg(bytes: Uint8Array): boolean {\n const head = new TextDecoder(\"utf-8\", { fatal: false })\n .decode(bytes.slice(0, 1024))\n .trimStart()\n .toLowerCase();\n\n if (head.startsWith(\"<svg\")) return true;\n if (head.startsWith(\"<?xml\") || head.startsWith(\"<!doctype svg\")) {\n return head.includes(\"<svg\");\n }\n return false;\n}\n\n// No HEIC: Astro's image service cannot read it, so a site could not show it.\nconst IMAGE_TYPES = new Set([\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\", \"image/avif\"]);\n\nconst DOCUMENT_TYPES = new Set([\"application/pdf\"]);\n\nexport const isImageType = (mimeType: string): boolean => IMAGE_TYPES.has(mimeType);\n\n/**\n * Whether a detected type is acceptable for a field's `accept` setting.\n * `any` deliberately does not mean \"anything\": only the types listed here can\n * be stored at all.\n */\nexport function isAccepted(mimeType: string, accept: \"image\" | \"document\" | \"any\"): boolean {\n if (accept === \"image\") return IMAGE_TYPES.has(mimeType);\n if (accept === \"document\") return DOCUMENT_TYPES.has(mimeType);\n return IMAGE_TYPES.has(mimeType) || DOCUMENT_TYPES.has(mimeType);\n}\n\n/** A video is large for a Git repository, and the media list cannot show one. */\nexport const VIDEO_TYPE = \"video/mp4\";\n\n/**\n * SVG can carry scripts, so Mica neither stores nor shows it (#86). It is still\n * detected, so that the refusal can say why.\n */\nexport const SVG_TYPE = \"image/svg+xml\";\n"]}
@@ -4,6 +4,6 @@
4
4
  * Git is the history, and `revision` makes concurrent edits safe.
5
5
  */
6
6
  import { Hono } from "hono";
7
- import type { Env } from "../http.js";
7
+ import { type Env } from "../http.js";
8
8
  export declare function adminContentRoutes(): Hono<Env>;
9
9
  //# sourceMappingURL=admin-content.d.ts.map
@@ -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;AAmCtC,wBAAgB,kBAAkB,IAAI,IAAI,CAAC,GAAG,CAAC,CAoJ9C"}
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,EAAY,KAAK,GAAG,EAAE,MAAM,YAAY,CAAC;AAuChD,wBAAgB,kBAAkB,IAAI,IAAI,CAAC,GAAG,CAAC,CA8J9C"}
@@ -9,6 +9,7 @@ import { collectionEntryPath, compareEntries, entryTitle, LayoutError, singleton
9
9
  import { Hono } from "hono";
10
10
  import { z } from "zod";
11
11
  import { ApiError, badRequest, notFound } from "../lib/errors.js";
12
+ import { editorOf } from "../http.js";
12
13
  const saveSchema = z.object({
13
14
  data: z.record(z.string(), z.unknown()),
14
15
  slug: z.string().max(120).optional(),
@@ -17,6 +18,10 @@ const saveSchema = z.object({
17
18
  * exist, so two parallel creates cannot overwrite.
18
19
  */
19
20
  revision: z.string().nullable().optional(),
21
+ /** The editor saw that a colleague saved since, and chose to save over it. Logged. */
22
+ keepMine: z.boolean().optional(),
23
+ /** On a URL change: other entries and settings pages that link here follow the new URL. */
24
+ updateLinks: z.boolean().optional(),
20
25
  });
21
26
  const reorderSchema = z.object({ slugs: z.array(z.string().min(1)).max(2000) });
22
27
  async function jsonBody(c) {
@@ -116,7 +121,14 @@ export function adminContentRoutes() {
116
121
  data: parsed.data.data,
117
122
  slug: c.req.param("slug"),
118
123
  expectedRevision: parsed.data.revision,
124
+ ...(parsed.data.updateLinks ? { updateLinks: true } : {}),
119
125
  }));
126
+ if (parsed.data.keepMine) {
127
+ c.get("ctx").logger.info("Kept own version over a newer save", {
128
+ entry: `${c.req.param("collection")}/${c.req.param("slug")}`,
129
+ editor: editorOf(c),
130
+ });
131
+ }
120
132
  return c.json(saved);
121
133
  });
122
134
  app.delete("/collections/:collection/entries/:slug", async (c) => {
@@ -148,6 +160,9 @@ export function adminContentRoutes() {
148
160
  const saved = await handled(() => content.saveSingleton(c.req.param("name"), body?.data ?? {}, {
149
161
  expectedRevision: body?.revision,
150
162
  }));
163
+ if (body?.keepMine === true) {
164
+ c.get("ctx").logger.info("Kept own version over a newer save", { singleton: c.req.param("name"), editor: editorOf(c) });
165
+ }
151
166
  return c.json(saved);
152
167
  });
153
168
  return app;
@@ -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 { 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
+ {"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 { editorOf, 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 /** The editor saw that a colleague saved since, and chose to save over it. Logged. */\n keepMine: z.boolean().optional(),\n /** On a URL change: other entries and settings pages that link here follow the new URL. */\n updateLinks: z.boolean().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 ...(parsed.data.updateLinks ? { updateLinks: true } : {}),\n }),\n );\n if (parsed.data.keepMine) {\n c.get(\"ctx\").logger.info(\"Kept own version over a newer save\", {\n entry: `${c.req.param(\"collection\")}/${c.req.param(\"slug\")}`,\n editor: editorOf(c),\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; keepMine?: boolean };\n\n const saved = await handled(() =>\n content.saveSingleton(c.req.param(\"name\"), body?.data ?? {}, {\n expectedRevision: body?.revision,\n }),\n );\n if (body?.keepMine === true) {\n c.get(\"ctx\").logger.info(\"Kept own version over a newer save\", { singleton: c.req.param(\"name\"), editor: editorOf(c) });\n }\n\n return c.json(saved);\n });\n\n return app;\n}\n"]}
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Admin media routes. Media is files in the content repository, and there is no delete endpoint.
3
- * An upload is written on its own, before the entry save, because the editor needs its path first.
2
+ * Admin media routes. Media is files in the content repository. An upload is
3
+ * written on its own, before the entry save, because the editor needs its path
4
+ * first. A delete is a pending change like any other, and is refused while
5
+ * something uses the file.
4
6
  */
5
7
  import { Hono } from "hono";
6
- import type { Env } from "../http.js";
8
+ import { type Env } from "../http.js";
7
9
  export declare function adminMediaRoutes(): Hono<Env>;
8
10
  //# sourceMappingURL=admin-media.d.ts.map
@@ -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,CAmF5C"}
1
+ {"version":3,"file":"admin-media.d.ts","sourceRoot":"","sources":["../../src/routes/admin-media.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAM5B,OAAO,EAAY,KAAK,GAAG,EAAE,MAAM,YAAY,CAAC;AA6BhD,wBAAgB,gBAAgB,IAAI,IAAI,CAAC,GAAG,CAAC,CA2H5C"}