@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.
- package/LICENSE +105 -0
- package/README.md +132 -0
- package/dist/boot.d.ts +15 -0
- package/dist/boot.js +33 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +45 -0
- package/dist/create-site/cli.d.ts +2 -0
- package/dist/create-site/cli.js +31 -0
- package/dist/create-site/generate-site.d.ts +9 -0
- package/dist/create-site/generate-site.js +109 -0
- package/dist/create-site/mint-token-cli.d.ts +2 -0
- package/dist/create-site/mint-token-cli.js +26 -0
- package/dist/create-site/mint-token.d.ts +7 -0
- package/dist/create-site/mint-token.js +49 -0
- package/dist/create-site/template/content/menus/footerCompany.json +9 -0
- package/dist/create-site/template/content/menus/footerProduct.json +9 -0
- package/dist/create-site/template/content/menus/footerResources.json +9 -0
- package/dist/create-site/template/content/menus/main.json +9 -0
- package/dist/create-site/template/content/pages/404.json +22 -0
- package/dist/create-site/template/content/pages/about/careers.json +21 -0
- package/dist/create-site/template/content/pages/about/team.json +21 -0
- package/dist/create-site/template/content/pages/about.json +21 -0
- package/dist/create-site/template/content/pages/docs/deployment.json +21 -0
- package/dist/create-site/template/content/pages/docs/getting-started/quickstart.json +21 -0
- package/dist/create-site/template/content/pages/docs/getting-started.json +23 -0
- package/dist/create-site/template/content/pages/docs.json +21 -0
- package/dist/create-site/template/content/pages/index.json +181 -0
- package/dist/create-site/template/content/redirects.json +4 -0
- package/dist/create-site/template/dockerignore +6 -0
- package/dist/create-site/template/gitignore +11 -0
- package/dist/create-site/template/theme/assets/site.js +49 -0
- package/dist/create-site/template/theme/assets/style.css +1153 -0
- package/dist/create-site/template/theme/blocks/button.liquid +21 -0
- package/dist/create-site/template/theme/blocks/comparison-row.liquid +19 -0
- package/dist/create-site/template/theme/blocks/faq-item.liquid +22 -0
- package/dist/create-site/template/theme/blocks/feature-card.liquid +23 -0
- package/dist/create-site/template/theme/blocks/logo-mark.liquid +13 -0
- package/dist/create-site/template/theme/blocks/pricing-tier.liquid +42 -0
- package/dist/create-site/template/theme/blocks/step-card.liquid +19 -0
- package/dist/create-site/template/theme/blocks/testimonial-card.liquid +25 -0
- package/dist/create-site/template/theme/layouts/theme.liquid +99 -0
- package/dist/create-site/template/theme/sections/comparison.liquid +41 -0
- package/dist/create-site/template/theme/sections/cta-banner.liquid +27 -0
- package/dist/create-site/template/theme/sections/faq.liquid +27 -0
- package/dist/create-site/template/theme/sections/feature-grid.liquid +27 -0
- package/dist/create-site/template/theme/sections/hero.liquid +36 -0
- package/dist/create-site/template/theme/sections/how-it-works.liquid +27 -0
- package/dist/create-site/template/theme/sections/pricing.liquid +27 -0
- package/dist/create-site/template/theme/sections/social-proof.liquid +21 -0
- package/dist/create-site/template/theme/sections/testimonials.liquid +27 -0
- package/dist/create-site/template/theme/snippets/icon.liquid +24 -0
- package/dist/create-site/template/theme/snippets/logo.liquid +7 -0
- package/dist/create-site/template/theme/snippets/site-name.liquid +1 -0
- package/dist/create-site/template/vhost/Dockerfile +33 -0
- package/dist/create-site/template/vhost/docker-entrypoint.sh +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/media/drivers/driver.d.ts +11 -0
- package/dist/media/drivers/driver.js +1 -0
- package/dist/media/drivers/local-fs-driver.d.ts +3 -0
- package/dist/media/drivers/local-fs-driver.js +81 -0
- package/dist/media/filename.d.ts +1 -0
- package/dist/media/filename.js +31 -0
- package/dist/media/manage-media.d.ts +19 -0
- package/dist/media/manage-media.js +45 -0
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +49 -0
- package/dist/renderer/engine.d.ts +2 -0
- package/dist/renderer/engine.js +48 -0
- package/dist/renderer/render-page.d.ts +28 -0
- package/dist/renderer/render-page.js +143 -0
- package/dist/renderer/theme-templates.d.ts +7 -0
- package/dist/renderer/theme-templates.js +82 -0
- package/dist/routes/assets.d.ts +6 -0
- package/dist/routes/assets.js +30 -0
- package/dist/routes/batch.d.ts +10 -0
- package/dist/routes/batch.js +148 -0
- package/dist/routes/capabilities.d.ts +5 -0
- package/dist/routes/capabilities.js +27 -0
- package/dist/routes/content.d.ts +8 -0
- package/dist/routes/content.js +166 -0
- package/dist/routes/drafts.d.ts +10 -0
- package/dist/routes/drafts.js +73 -0
- package/dist/routes/git.d.ts +8 -0
- package/dist/routes/git.js +158 -0
- package/dist/routes/index.d.ts +19 -0
- package/dist/routes/index.js +84 -0
- package/dist/routes/media-public.d.ts +5 -0
- package/dist/routes/media-public.js +48 -0
- package/dist/routes/media.d.ts +9 -0
- package/dist/routes/media.js +84 -0
- package/dist/routes/menus.d.ts +8 -0
- package/dist/routes/menus.js +77 -0
- package/dist/routes/preview-revision.d.ts +13 -0
- package/dist/routes/preview-revision.js +109 -0
- package/dist/routes/preview.d.ts +13 -0
- package/dist/routes/preview.js +52 -0
- package/dist/routes/public.d.ts +11 -0
- package/dist/routes/public.js +117 -0
- package/dist/routes/publish.d.ts +10 -0
- package/dist/routes/publish.js +105 -0
- package/dist/routes/redirects.d.ts +8 -0
- package/dist/routes/redirects.js +132 -0
- package/dist/routes/search.d.ts +8 -0
- package/dist/routes/search.js +11 -0
- package/dist/routes/sitemap.d.ts +6 -0
- package/dist/routes/sitemap.js +79 -0
- package/dist/routes/theme.d.ts +10 -0
- package/dist/routes/theme.js +17 -0
- package/dist/schemas/instance.schema.json +17 -0
- package/dist/schemas/menu.schema.json +23 -0
- package/dist/schemas/page.schema.json +20 -0
- package/dist/schemas/post.schema.json +25 -0
- package/dist/schemas/redirects.schema.json +24 -0
- package/dist/search/drivers/driver.d.ts +9 -0
- package/dist/search/drivers/driver.js +1 -0
- package/dist/search/drivers/node-sqlite-driver.d.ts +3 -0
- package/dist/search/drivers/node-sqlite-driver.js +19 -0
- package/dist/search/query-index.d.ts +5 -0
- package/dist/search/query-index.js +21 -0
- package/dist/search/rebuild-index.d.ts +2 -0
- package/dist/search/rebuild-index.js +111 -0
- package/dist/server-config.d.ts +22 -0
- package/dist/server-config.js +196 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +163 -0
- package/dist/services/batch.d.ts +35 -0
- package/dist/services/batch.js +136 -0
- package/dist/services/checkpoint.d.ts +4 -0
- package/dist/services/checkpoint.js +45 -0
- package/dist/services/content-read.d.ts +27 -0
- package/dist/services/content-read.js +156 -0
- package/dist/services/delete-content.d.ts +12 -0
- package/dist/services/delete-content.js +174 -0
- package/dist/services/dev-tunnel.d.ts +5 -0
- package/dist/services/dev-tunnel.js +5 -0
- package/dist/services/drafts.d.ts +15 -0
- package/dist/services/drafts.js +155 -0
- package/dist/services/etag.d.ts +2 -0
- package/dist/services/etag.js +30 -0
- package/dist/services/fs-walk.d.ts +1 -0
- package/dist/services/fs-walk.js +26 -0
- package/dist/services/git-commit.d.ts +3 -0
- package/dist/services/git-commit.js +12 -0
- package/dist/services/git-history.d.ts +28 -0
- package/dist/services/git-history.js +131 -0
- package/dist/services/git-revert.d.ts +12 -0
- package/dist/services/git-revert.js +140 -0
- package/dist/services/git.d.ts +16 -0
- package/dist/services/git.js +127 -0
- package/dist/services/interval-job.d.ts +4 -0
- package/dist/services/interval-job.js +9 -0
- package/dist/services/ip-allowlist.d.ts +7 -0
- package/dist/services/ip-allowlist.js +31 -0
- package/dist/services/manage-menus.d.ts +10 -0
- package/dist/services/manage-menus.js +84 -0
- package/dist/services/manage-redirects.d.ts +25 -0
- package/dist/services/manage-redirects.js +166 -0
- package/dist/services/menus.d.ts +8 -0
- package/dist/services/menus.js +42 -0
- package/dist/services/migration-runner.d.ts +13 -0
- package/dist/services/migration-runner.js +123 -0
- package/dist/services/mime-types.d.ts +3 -0
- package/dist/services/mime-types.js +36 -0
- package/dist/services/move.d.ts +16 -0
- package/dist/services/move.js +175 -0
- package/dist/services/path-safety.d.ts +6 -0
- package/dist/services/path-safety.js +112 -0
- package/dist/services/post-urls.d.ts +3 -0
- package/dist/services/post-urls.js +27 -0
- package/dist/services/prepared-operation.d.ts +4 -0
- package/dist/services/prepared-operation.js +1 -0
- package/dist/services/publish.d.ts +14 -0
- package/dist/services/publish.js +254 -0
- package/dist/services/rate-limit-config.d.ts +9 -0
- package/dist/services/rate-limit-config.js +20 -0
- package/dist/services/redirects.d.ts +27 -0
- package/dist/services/redirects.js +175 -0
- package/dist/services/resolve-blog-url.d.ts +11 -0
- package/dist/services/resolve-blog-url.js +31 -0
- package/dist/services/resolve-url.d.ts +11 -0
- package/dist/services/resolve-url.js +20 -0
- package/dist/services/startup-checks.d.ts +10 -0
- package/dist/services/startup-checks.js +81 -0
- package/dist/services/static-file.d.ts +7 -0
- package/dist/services/static-file.js +37 -0
- package/dist/services/theme-component-file.d.ts +5 -0
- package/dist/services/theme-component-file.js +32 -0
- package/dist/services/theme-page-templates.d.ts +7 -0
- package/dist/services/theme-page-templates.js +53 -0
- package/dist/services/theme-schemas.d.ts +2 -0
- package/dist/services/theme-schemas.js +63 -0
- package/dist/services/token-auth.d.ts +10 -0
- package/dist/services/token-auth.js +64 -0
- package/dist/services/urls.d.ts +2 -0
- package/dist/services/urls.js +20 -0
- package/dist/services/validation.d.ts +24 -0
- package/dist/services/validation.js +158 -0
- package/dist/services/write-queue.d.ts +1 -0
- package/dist/services/write-queue.js +14 -0
- 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,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
|
+
}
|