@o-a/cms-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +132 -0
  3. package/dist/boot.d.ts +15 -0
  4. package/dist/boot.js +33 -0
  5. package/dist/config.d.ts +19 -0
  6. package/dist/config.js +45 -0
  7. package/dist/create-site/cli.d.ts +2 -0
  8. package/dist/create-site/cli.js +31 -0
  9. package/dist/create-site/generate-site.d.ts +9 -0
  10. package/dist/create-site/generate-site.js +109 -0
  11. package/dist/create-site/mint-token-cli.d.ts +2 -0
  12. package/dist/create-site/mint-token-cli.js +26 -0
  13. package/dist/create-site/mint-token.d.ts +7 -0
  14. package/dist/create-site/mint-token.js +49 -0
  15. package/dist/create-site/template/content/menus/footerCompany.json +9 -0
  16. package/dist/create-site/template/content/menus/footerProduct.json +9 -0
  17. package/dist/create-site/template/content/menus/footerResources.json +9 -0
  18. package/dist/create-site/template/content/menus/main.json +9 -0
  19. package/dist/create-site/template/content/pages/404.json +22 -0
  20. package/dist/create-site/template/content/pages/about/careers.json +21 -0
  21. package/dist/create-site/template/content/pages/about/team.json +21 -0
  22. package/dist/create-site/template/content/pages/about.json +21 -0
  23. package/dist/create-site/template/content/pages/docs/deployment.json +21 -0
  24. package/dist/create-site/template/content/pages/docs/getting-started/quickstart.json +21 -0
  25. package/dist/create-site/template/content/pages/docs/getting-started.json +23 -0
  26. package/dist/create-site/template/content/pages/docs.json +21 -0
  27. package/dist/create-site/template/content/pages/index.json +181 -0
  28. package/dist/create-site/template/content/redirects.json +4 -0
  29. package/dist/create-site/template/dockerignore +6 -0
  30. package/dist/create-site/template/gitignore +11 -0
  31. package/dist/create-site/template/theme/assets/site.js +49 -0
  32. package/dist/create-site/template/theme/assets/style.css +1153 -0
  33. package/dist/create-site/template/theme/blocks/button.liquid +21 -0
  34. package/dist/create-site/template/theme/blocks/comparison-row.liquid +19 -0
  35. package/dist/create-site/template/theme/blocks/faq-item.liquid +22 -0
  36. package/dist/create-site/template/theme/blocks/feature-card.liquid +23 -0
  37. package/dist/create-site/template/theme/blocks/logo-mark.liquid +13 -0
  38. package/dist/create-site/template/theme/blocks/pricing-tier.liquid +42 -0
  39. package/dist/create-site/template/theme/blocks/step-card.liquid +19 -0
  40. package/dist/create-site/template/theme/blocks/testimonial-card.liquid +25 -0
  41. package/dist/create-site/template/theme/layouts/theme.liquid +99 -0
  42. package/dist/create-site/template/theme/sections/comparison.liquid +41 -0
  43. package/dist/create-site/template/theme/sections/cta-banner.liquid +27 -0
  44. package/dist/create-site/template/theme/sections/faq.liquid +27 -0
  45. package/dist/create-site/template/theme/sections/feature-grid.liquid +27 -0
  46. package/dist/create-site/template/theme/sections/hero.liquid +36 -0
  47. package/dist/create-site/template/theme/sections/how-it-works.liquid +27 -0
  48. package/dist/create-site/template/theme/sections/pricing.liquid +27 -0
  49. package/dist/create-site/template/theme/sections/social-proof.liquid +21 -0
  50. package/dist/create-site/template/theme/sections/testimonials.liquid +27 -0
  51. package/dist/create-site/template/theme/snippets/icon.liquid +24 -0
  52. package/dist/create-site/template/theme/snippets/logo.liquid +7 -0
  53. package/dist/create-site/template/theme/snippets/site-name.liquid +1 -0
  54. package/dist/create-site/template/vhost/Dockerfile +33 -0
  55. package/dist/create-site/template/vhost/docker-entrypoint.sh +30 -0
  56. package/dist/index.d.ts +2 -0
  57. package/dist/index.js +4 -0
  58. package/dist/media/drivers/driver.d.ts +11 -0
  59. package/dist/media/drivers/driver.js +1 -0
  60. package/dist/media/drivers/local-fs-driver.d.ts +3 -0
  61. package/dist/media/drivers/local-fs-driver.js +81 -0
  62. package/dist/media/filename.d.ts +1 -0
  63. package/dist/media/filename.js +31 -0
  64. package/dist/media/manage-media.d.ts +19 -0
  65. package/dist/media/manage-media.js +45 -0
  66. package/dist/migrations/index.d.ts +3 -0
  67. package/dist/migrations/index.js +49 -0
  68. package/dist/renderer/engine.d.ts +2 -0
  69. package/dist/renderer/engine.js +48 -0
  70. package/dist/renderer/render-page.d.ts +28 -0
  71. package/dist/renderer/render-page.js +143 -0
  72. package/dist/renderer/theme-templates.d.ts +7 -0
  73. package/dist/renderer/theme-templates.js +82 -0
  74. package/dist/routes/assets.d.ts +6 -0
  75. package/dist/routes/assets.js +30 -0
  76. package/dist/routes/batch.d.ts +10 -0
  77. package/dist/routes/batch.js +148 -0
  78. package/dist/routes/capabilities.d.ts +5 -0
  79. package/dist/routes/capabilities.js +27 -0
  80. package/dist/routes/content.d.ts +8 -0
  81. package/dist/routes/content.js +166 -0
  82. package/dist/routes/drafts.d.ts +10 -0
  83. package/dist/routes/drafts.js +73 -0
  84. package/dist/routes/git.d.ts +8 -0
  85. package/dist/routes/git.js +158 -0
  86. package/dist/routes/index.d.ts +19 -0
  87. package/dist/routes/index.js +84 -0
  88. package/dist/routes/media-public.d.ts +5 -0
  89. package/dist/routes/media-public.js +48 -0
  90. package/dist/routes/media.d.ts +9 -0
  91. package/dist/routes/media.js +84 -0
  92. package/dist/routes/menus.d.ts +8 -0
  93. package/dist/routes/menus.js +77 -0
  94. package/dist/routes/preview-revision.d.ts +13 -0
  95. package/dist/routes/preview-revision.js +109 -0
  96. package/dist/routes/preview.d.ts +13 -0
  97. package/dist/routes/preview.js +52 -0
  98. package/dist/routes/public.d.ts +11 -0
  99. package/dist/routes/public.js +117 -0
  100. package/dist/routes/publish.d.ts +10 -0
  101. package/dist/routes/publish.js +105 -0
  102. package/dist/routes/redirects.d.ts +8 -0
  103. package/dist/routes/redirects.js +132 -0
  104. package/dist/routes/search.d.ts +8 -0
  105. package/dist/routes/search.js +11 -0
  106. package/dist/routes/sitemap.d.ts +6 -0
  107. package/dist/routes/sitemap.js +79 -0
  108. package/dist/routes/theme.d.ts +10 -0
  109. package/dist/routes/theme.js +17 -0
  110. package/dist/schemas/instance.schema.json +17 -0
  111. package/dist/schemas/menu.schema.json +23 -0
  112. package/dist/schemas/page.schema.json +20 -0
  113. package/dist/schemas/post.schema.json +25 -0
  114. package/dist/schemas/redirects.schema.json +24 -0
  115. package/dist/search/drivers/driver.d.ts +9 -0
  116. package/dist/search/drivers/driver.js +1 -0
  117. package/dist/search/drivers/node-sqlite-driver.d.ts +3 -0
  118. package/dist/search/drivers/node-sqlite-driver.js +19 -0
  119. package/dist/search/query-index.d.ts +5 -0
  120. package/dist/search/query-index.js +21 -0
  121. package/dist/search/rebuild-index.d.ts +2 -0
  122. package/dist/search/rebuild-index.js +111 -0
  123. package/dist/server-config.d.ts +22 -0
  124. package/dist/server-config.js +196 -0
  125. package/dist/server.d.ts +13 -0
  126. package/dist/server.js +163 -0
  127. package/dist/services/batch.d.ts +35 -0
  128. package/dist/services/batch.js +136 -0
  129. package/dist/services/checkpoint.d.ts +4 -0
  130. package/dist/services/checkpoint.js +45 -0
  131. package/dist/services/content-read.d.ts +27 -0
  132. package/dist/services/content-read.js +156 -0
  133. package/dist/services/delete-content.d.ts +12 -0
  134. package/dist/services/delete-content.js +174 -0
  135. package/dist/services/dev-tunnel.d.ts +5 -0
  136. package/dist/services/dev-tunnel.js +5 -0
  137. package/dist/services/drafts.d.ts +15 -0
  138. package/dist/services/drafts.js +155 -0
  139. package/dist/services/etag.d.ts +2 -0
  140. package/dist/services/etag.js +30 -0
  141. package/dist/services/fs-walk.d.ts +1 -0
  142. package/dist/services/fs-walk.js +26 -0
  143. package/dist/services/git-commit.d.ts +3 -0
  144. package/dist/services/git-commit.js +12 -0
  145. package/dist/services/git-history.d.ts +28 -0
  146. package/dist/services/git-history.js +131 -0
  147. package/dist/services/git-revert.d.ts +12 -0
  148. package/dist/services/git-revert.js +140 -0
  149. package/dist/services/git.d.ts +16 -0
  150. package/dist/services/git.js +127 -0
  151. package/dist/services/interval-job.d.ts +4 -0
  152. package/dist/services/interval-job.js +9 -0
  153. package/dist/services/ip-allowlist.d.ts +7 -0
  154. package/dist/services/ip-allowlist.js +31 -0
  155. package/dist/services/manage-menus.d.ts +10 -0
  156. package/dist/services/manage-menus.js +84 -0
  157. package/dist/services/manage-redirects.d.ts +25 -0
  158. package/dist/services/manage-redirects.js +166 -0
  159. package/dist/services/menus.d.ts +8 -0
  160. package/dist/services/menus.js +42 -0
  161. package/dist/services/migration-runner.d.ts +13 -0
  162. package/dist/services/migration-runner.js +123 -0
  163. package/dist/services/mime-types.d.ts +3 -0
  164. package/dist/services/mime-types.js +36 -0
  165. package/dist/services/move.d.ts +16 -0
  166. package/dist/services/move.js +175 -0
  167. package/dist/services/path-safety.d.ts +6 -0
  168. package/dist/services/path-safety.js +112 -0
  169. package/dist/services/post-urls.d.ts +3 -0
  170. package/dist/services/post-urls.js +27 -0
  171. package/dist/services/prepared-operation.d.ts +4 -0
  172. package/dist/services/prepared-operation.js +1 -0
  173. package/dist/services/publish.d.ts +14 -0
  174. package/dist/services/publish.js +254 -0
  175. package/dist/services/rate-limit-config.d.ts +9 -0
  176. package/dist/services/rate-limit-config.js +20 -0
  177. package/dist/services/redirects.d.ts +27 -0
  178. package/dist/services/redirects.js +175 -0
  179. package/dist/services/resolve-blog-url.d.ts +11 -0
  180. package/dist/services/resolve-blog-url.js +31 -0
  181. package/dist/services/resolve-url.d.ts +11 -0
  182. package/dist/services/resolve-url.js +20 -0
  183. package/dist/services/startup-checks.d.ts +10 -0
  184. package/dist/services/startup-checks.js +81 -0
  185. package/dist/services/static-file.d.ts +7 -0
  186. package/dist/services/static-file.js +37 -0
  187. package/dist/services/theme-component-file.d.ts +5 -0
  188. package/dist/services/theme-component-file.js +32 -0
  189. package/dist/services/theme-page-templates.d.ts +7 -0
  190. package/dist/services/theme-page-templates.js +53 -0
  191. package/dist/services/theme-schemas.d.ts +2 -0
  192. package/dist/services/theme-schemas.js +63 -0
  193. package/dist/services/token-auth.d.ts +10 -0
  194. package/dist/services/token-auth.js +64 -0
  195. package/dist/services/urls.d.ts +2 -0
  196. package/dist/services/urls.js +20 -0
  197. package/dist/services/validation.d.ts +24 -0
  198. package/dist/services/validation.js +158 -0
  199. package/dist/services/write-queue.d.ts +1 -0
  200. package/dist/services/write-queue.js +14 -0
  201. package/package.json +53 -0
@@ -0,0 +1,64 @@
1
+ import { createHash, timingSafeEqual } from 'node:crypto';
2
+ // The first domain error in this codebase's established XxxError+
3
+ // reason convention that has to cross the Fastify HTTP boundary -
4
+ // every prior one throws pre-HTTP (at boot) or predates Phase 2
5
+ // entirely. statusCode is set explicitly in the constructor so the
6
+ // existing global error handler (server.ts, which branches on
7
+ // error.statusCode) passes these through with their real message
8
+ // intact, never sanitising them as a generic 500 - confirmed
9
+ // empirically that a preHandler-thrown error with .statusCode set
10
+ // reaches that same handler correctly.
11
+ export class AuthError extends Error {
12
+ reason;
13
+ statusCode;
14
+ constructor(reason, message) {
15
+ super(message);
16
+ this.name = 'AuthError';
17
+ this.reason = reason;
18
+ this.statusCode = reason === 'missing-scope' ? 403 : 401;
19
+ }
20
+ }
21
+ const BEARER_PREFIX = 'Bearer ';
22
+ function hashToken(token) {
23
+ return createHash('sha256').update(token).digest();
24
+ }
25
+ // Never compare token digests (or hex strings) with === or
26
+ // Buffer.equals() - neither is guaranteed constant-time. Only
27
+ // timingSafeEqual, which is why both sides must first be reduced to
28
+ // a fixed-length digest: it throws on mismatched buffer lengths, and
29
+ // a raw incoming token is never the same length as another token in
30
+ // general (confirmed empirically before this was written).
31
+ export function verifyToken(tokens, presentedToken) {
32
+ const presentedDigest = hashToken(presentedToken);
33
+ for (const entry of tokens) {
34
+ const storedDigest = Buffer.from(entry.hash, 'hex');
35
+ if (timingSafeEqual(presentedDigest, storedDigest)) {
36
+ return entry.scopes;
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+ // Applied as a per-route preHandler option
42
+ // (fastify.get(path, { preHandler: requireScope(tokens, 'content') }, handler)),
43
+ // not a whole-file addHook: a single route-group file may eventually
44
+ // mix scopes (e.g. a content-scoped GET and a media-scoped POST in
45
+ // the same file), so the check is granular to the route, not the
46
+ // file. Each route-group file still registers plainly, never wrapped
47
+ // in fastify-plugin's fp(), preserving the per-plugin encapsulation
48
+ // this design depends on.
49
+ export function requireScope(tokens, scope) {
50
+ return async function scopeGuard(request) {
51
+ const header = request.headers.authorization;
52
+ if (!header || !header.startsWith(BEARER_PREFIX)) {
53
+ throw new AuthError('missing-token', 'Missing or malformed Authorization header');
54
+ }
55
+ const presentedToken = header.slice(BEARER_PREFIX.length);
56
+ const scopes = verifyToken(tokens, presentedToken);
57
+ if (scopes === null) {
58
+ throw new AuthError('invalid-token', 'The presented token does not match any configured token');
59
+ }
60
+ if (!scopes.includes(scope)) {
61
+ throw new AuthError('missing-scope', `This route requires the "${scope}" scope`);
62
+ }
63
+ };
64
+ }
@@ -0,0 +1,2 @@
1
+ export declare function urlToPagePath(url: string): string;
2
+ export declare function pagePathToUrl(relativePagePath: string): string;
@@ -0,0 +1,20 @@
1
+ // Pure, filesystem-free mapping between a public URL and a page's path
2
+ // relative to pagesRoot. A page with children is a file (about.json)
3
+ // sitting alongside a same-named sibling directory (about/) holding
4
+ // descendants, so this mapping never needs to know whether a given
5
+ // page currently has children - unlike an about/index.json convention,
6
+ // which would make resolution filesystem-dependent.
7
+ export function urlToPagePath(url) {
8
+ const trimmed = url.replace(/^\/+/, '').replace(/\/+$/, '');
9
+ if (trimmed === '') {
10
+ return 'index.json';
11
+ }
12
+ return `${trimmed}.json`;
13
+ }
14
+ export function pagePathToUrl(relativePagePath) {
15
+ const withoutExtension = relativePagePath.replace(/\.json$/, '');
16
+ if (withoutExtension === 'index') {
17
+ return '/';
18
+ }
19
+ return `/${withoutExtension}`;
20
+ }
@@ -0,0 +1,24 @@
1
+ export interface ValidationError {
2
+ path: string;
3
+ message: string;
4
+ keyword: string;
5
+ }
6
+ export interface ValidationResult {
7
+ valid: boolean;
8
+ errors: ValidationError[];
9
+ }
10
+ export interface ThemeSchemas {
11
+ sections: Record<string, object>;
12
+ blocks: Record<string, object>;
13
+ acceptsBlocks: {
14
+ sections: Record<string, boolean>;
15
+ blocks: Record<string, boolean>;
16
+ };
17
+ }
18
+ export declare function requiredFieldsHaveValidDefaults(schema: object): boolean;
19
+ export declare function validateInstance(instance: unknown, kind: 'section' | 'block', themeSchemas: ThemeSchemas): ValidationResult;
20
+ export declare function validatePage(page: unknown, themeSchemas: ThemeSchemas): ValidationResult;
21
+ export declare function validatePost(post: unknown, themeSchemas: ThemeSchemas): ValidationResult;
22
+ export declare function validateMenu(menu: unknown): ValidationResult;
23
+ export declare function validateRedirects(redirects: unknown): ValidationResult;
24
+ export declare function validateContent(relativePath: string, content: unknown, themeSchemas: ThemeSchemas): ValidationResult;
@@ -0,0 +1,158 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { Ajv } from 'ajv';
4
+ // strict: false because theme schema.json files are authored by theme
5
+ // authors, not agent code, and are validated the same lenient way
6
+ // everywhere (see theme-schemas.ts).
7
+ const ajv = new Ajv({ allErrors: true, strict: false });
8
+ const schemasDir = join(import.meta.dirname, '..', 'schemas');
9
+ function readSchema(filename) {
10
+ return JSON.parse(readFileSync(join(schemasDir, filename), 'utf-8'));
11
+ }
12
+ const instanceSchema = readSchema('instance.schema.json');
13
+ ajv.addSchema(instanceSchema);
14
+ const validatePageEnvelope = ajv.compile(readSchema('page.schema.json'));
15
+ const validatePostEnvelope = ajv.compile(readSchema('post.schema.json'));
16
+ const validateMenuEnvelope = ajv.compile(readSchema('menu.schema.json'));
17
+ const validateRedirectsEnvelope = ajv.compile(readSchema('redirects.schema.json'));
18
+ const validateInstanceEnvelope = ajv.compile(instanceSchema);
19
+ function normaliseErrors(errors) {
20
+ if (!errors) {
21
+ return [];
22
+ }
23
+ return errors.map((error) => {
24
+ let path = error.instancePath;
25
+ if (error.keyword === 'required') {
26
+ const { missingProperty } = error.params;
27
+ path = `${error.instancePath}/${missingProperty}`;
28
+ }
29
+ else if (error.keyword === 'additionalProperties') {
30
+ const { additionalProperty } = error.params;
31
+ path = `${error.instancePath}/${additionalProperty}`;
32
+ }
33
+ return { path, message: error.message ?? '', keyword: error.keyword };
34
+ });
35
+ }
36
+ function runValidator(validate, data) {
37
+ const valid = validate(data);
38
+ return { valid: Boolean(valid), errors: normaliseErrors(validate.errors) };
39
+ }
40
+ function prefixErrors(errors, prefix) {
41
+ return errors.map((error) => ({ ...error, path: `${prefix}${error.path}` }));
42
+ }
43
+ // The theme-authored schema JSON is passed through verbatim end to end
44
+ // (see theme-schemas.ts), so an optional allowedBlocks array rides
45
+ // through the whole pipeline for free, exactly like the "title" keyword
46
+ // already does - never a real Ajv keyword, just a plain top-level
47
+ // annotation read directly off the object. Absent/malformed means
48
+ // unrestricted, matching every theme file written before this existed.
49
+ function allowedBlockTypesOf(schema) {
50
+ const value = schema?.allowedBlocks;
51
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'string') ? value : undefined;
52
+ }
53
+ // A theme author's own settings schema must declare a "default" for
54
+ // every property it lists as required, and that default must itself
55
+ // satisfy the property's own sub-schema - a bare "default": "" against
56
+ // a minLength: 1 field would technically have a default but still
57
+ // leave a pre-filled instance invalid, defeating the reason this
58
+ // exists (see theme-schemas.ts, which skips a type failing this check
59
+ // exactly like it already skips a malformed {% schema %} block).
60
+ export function requiredFieldsHaveValidDefaults(schema) {
61
+ const { required, properties } = schema;
62
+ if (!Array.isArray(required)) {
63
+ return true;
64
+ }
65
+ const propertyMap = (properties && typeof properties === 'object' ? properties : {});
66
+ return required.every((name) => {
67
+ if (typeof name !== 'string') {
68
+ return false;
69
+ }
70
+ const propertySchema = propertyMap[name];
71
+ if (typeof propertySchema !== 'object' || propertySchema === null || !('default' in propertySchema)) {
72
+ return false;
73
+ }
74
+ return ajv.validate(propertySchema, propertySchema.default) === true;
75
+ });
76
+ }
77
+ export function validateInstance(instance, kind, themeSchemas) {
78
+ const envelopeResult = runValidator(validateInstanceEnvelope, instance);
79
+ if (!envelopeResult.valid) {
80
+ return envelopeResult;
81
+ }
82
+ const typed = instance;
83
+ const typeSchemas = kind === 'section' ? themeSchemas.sections : themeSchemas.blocks;
84
+ const typeSchema = typeSchemas[typed.type];
85
+ const errors = [];
86
+ if (!typeSchema) {
87
+ errors.push({
88
+ path: '/type',
89
+ message: `Unknown ${kind} type "${typed.type}"`,
90
+ keyword: 'unknownType',
91
+ });
92
+ }
93
+ else {
94
+ const settingsValidate = ajv.compile(typeSchema);
95
+ const settingsResult = runValidator(settingsValidate, typed.settings);
96
+ errors.push(...prefixErrors(settingsResult.errors, '/settings'));
97
+ }
98
+ if (typed.blocks) {
99
+ const allowedBlocks = allowedBlockTypesOf(typeSchema);
100
+ typed.blocks.forEach((block, index) => {
101
+ const blockType = block?.type;
102
+ if (allowedBlocks && typeof blockType === 'string' && !allowedBlocks.includes(blockType)) {
103
+ errors.push({
104
+ path: `/blocks/${index}/type`,
105
+ message: `Block type "${blockType}" is not allowed here`,
106
+ keyword: 'disallowedBlockType',
107
+ });
108
+ }
109
+ const blockResult = validateInstance(block, 'block', themeSchemas);
110
+ errors.push(...prefixErrors(blockResult.errors, `/blocks/${index}`));
111
+ });
112
+ }
113
+ return { valid: errors.length === 0, errors };
114
+ }
115
+ // Shared by validatePage and validatePost: both envelopes require an
116
+ // identical sections/blocks recursion once their own envelope-level
117
+ // shape is confirmed valid - genuinely load-bearing for two real
118
+ // content types now, not a speculative abstraction.
119
+ function validateSectionedContent(envelopeValidate, content, themeSchemas) {
120
+ const envelopeResult = runValidator(envelopeValidate, content);
121
+ if (!envelopeResult.valid) {
122
+ return envelopeResult;
123
+ }
124
+ const typed = content;
125
+ const errors = [];
126
+ typed.sections.forEach((section, index) => {
127
+ const sectionResult = validateInstance(section, 'section', themeSchemas);
128
+ errors.push(...prefixErrors(sectionResult.errors, `/sections/${index}`));
129
+ });
130
+ return { valid: errors.length === 0, errors };
131
+ }
132
+ export function validatePage(page, themeSchemas) {
133
+ return validateSectionedContent(validatePageEnvelope, page, themeSchemas);
134
+ }
135
+ export function validatePost(post, themeSchemas) {
136
+ return validateSectionedContent(validatePostEnvelope, post, themeSchemas);
137
+ }
138
+ export function validateMenu(menu) {
139
+ return runValidator(validateMenuEnvelope, menu);
140
+ }
141
+ // Never routed through validateContent's dispatcher below: redirects.json
142
+ // lives at the site root, not under content/, so it never has a
143
+ // content/-relative path for that dispatcher to match against.
144
+ export function validateRedirects(redirects) {
145
+ return runValidator(validateRedirectsEnvelope, redirects);
146
+ }
147
+ // Dispatches on the relative path's prefix, not an in-file `type`
148
+ // field: a menu has no `type` field at all, and the correct schema to
149
+ // parse against must be known before the content is even validated.
150
+ export function validateContent(relativePath, content, themeSchemas) {
151
+ if (relativePath.startsWith('menus/')) {
152
+ return validateMenu(content);
153
+ }
154
+ if (relativePath.startsWith('posts/')) {
155
+ return validatePost(content, themeSchemas);
156
+ }
157
+ return validatePage(content, themeSchemas);
158
+ }
@@ -0,0 +1 @@
1
+ export declare function enqueue<T>(job: () => Promise<T>): Promise<T>;
@@ -0,0 +1,14 @@
1
+ // The single in-process write queue: all disk and git mutations go
2
+ // through enqueue(), one job at a time (constraint 6). Ordering only -
3
+ // no domain knowledge of what "clean" means for a failed job. That is
4
+ // each job's own responsibility (see drafts.ts, publish.ts).
5
+ //
6
+ // In-memory only, does not survive restart: the API only reports
7
+ // success after a write (and commit, where applicable) completes, so an
8
+ // in-flight request during a crash simply fails and the caller retries.
9
+ let tail = Promise.resolve();
10
+ export function enqueue(job) {
11
+ const result = tail.then(job, job);
12
+ tail = result.then(() => undefined, () => undefined);
13
+ return result;
14
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@o-a/cms-agent",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "description": "Self-hosted, git-backed CMS agent, distributed as a compiled npm package.",
9
+ "license": "SEE LICENSE IN LICENSE",
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "bin": {
20
+ "create-site": "dist/create-site/cli.js",
21
+ "mint-token": "dist/create-site/mint-token-cli.js"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "engines": {
27
+ "node": ">=22.6.0"
28
+ },
29
+ "scripts": {
30
+ "typecheck": "tsc --noEmit -p tsconfig.json",
31
+ "lint": "eslint .",
32
+ "test": "node --experimental-strip-types --test",
33
+ "test:packaging": "node --experimental-strip-types --test e2e/create-site-packaging.check.ts",
34
+ "build": "tsc -p tsconfig.build.json && rm -rf dist/schemas dist/create-site/template && cp -r src/schemas/. dist/schemas/ && cp -r src/create-site/template/. dist/create-site/template/",
35
+ "prepack": "npm run build"
36
+ },
37
+ "devDependencies": {
38
+ "@eslint/js": "^9.39.5",
39
+ "@types/localtunnel": "^2.0.4",
40
+ "@types/node": "^22.20.1",
41
+ "eslint": "^9.39.5",
42
+ "typescript": "^5.9.3",
43
+ "typescript-eslint": "^8.65.0"
44
+ },
45
+ "dependencies": {
46
+ "@fastify/multipart": "^10.1.0",
47
+ "@fastify/rate-limit": "^11.1.0",
48
+ "ajv": "^8.20.0",
49
+ "fastify": "^5.10.0",
50
+ "liquidjs": "10.27.2",
51
+ "localtunnel": "^2.0.2"
52
+ }
53
+ }