@minamocms/framework 0.0.0-stage → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +234 -0
- package/LICENSE +105 -0
- package/README.md +133 -2
- package/dist/boot.d.ts +19 -0
- package/dist/boot.js +22 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +44 -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 +12 -0
- package/dist/create-site/generate-site.js +168 -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/AGENTS.md +473 -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 +23 -0
- package/dist/create-site/template/content/pages/about/careers.json +22 -0
- package/dist/create-site/template/content/pages/about/team.json +22 -0
- package/dist/create-site/template/content/pages/about.json +22 -0
- package/dist/create-site/template/content/pages/docs/deployment.json +22 -0
- package/dist/create-site/template/content/pages/docs/getting-started/quickstart.json +22 -0
- package/dist/create-site/template/content/pages/docs/getting-started.json +24 -0
- package/dist/create-site/template/content/pages/docs.json +22 -0
- package/dist/create-site/template/content/pages/index.json +182 -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 +1223 -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/config/site_settings.json +11 -0
- package/dist/create-site/template/theme/layouts/theme.liquid +102 -0
- package/dist/create-site/template/theme/sections/comparison.liquid +41 -0
- package/dist/create-site/template/theme/sections/cta-banner.liquid +36 -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 +48 -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/responsive-media.liquid +107 -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 +58 -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 +2 -0
- package/dist/media/filename.js +54 -0
- package/dist/media/manage-media.d.ts +19 -0
- package/dist/media/manage-media.js +45 -0
- package/dist/media/seed-media-cli.d.ts +2 -0
- package/dist/media/seed-media-cli.js +22 -0
- package/dist/media/seed-media.d.ts +11 -0
- package/dist/media/seed-media.js +76 -0
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +83 -0
- package/dist/pull-site/cli.d.ts +2 -0
- package/dist/pull-site/cli.js +78 -0
- package/dist/pull-site/prompt-token.d.ts +16 -0
- package/dist/pull-site/prompt-token.js +69 -0
- package/dist/pull-site/prompts.d.ts +22 -0
- package/dist/pull-site/prompts.js +127 -0
- package/dist/pull-site/pull-site.d.ts +23 -0
- package/dist/pull-site/pull-site.js +211 -0
- package/dist/renderer/engine.d.ts +2 -0
- package/dist/renderer/engine.js +48 -0
- package/dist/renderer/render-cache.d.ts +11 -0
- package/dist/renderer/render-cache.js +11 -0
- package/dist/renderer/render-page.d.ts +34 -0
- package/dist/renderer/render-page.js +209 -0
- package/dist/renderer/theme-templates.d.ts +7 -0
- package/dist/renderer/theme-templates.js +82 -0
- package/dist/routes/admin-redirect.d.ts +5 -0
- package/dist/routes/admin-redirect.js +26 -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 +6 -0
- package/dist/routes/capabilities.js +27 -0
- package/dist/routes/content.d.ts +8 -0
- package/dist/routes/content.js +169 -0
- package/dist/routes/deleted-pages.d.ts +8 -0
- package/dist/routes/deleted-pages.js +9 -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 +12 -0
- package/dist/routes/index.js +86 -0
- package/dist/routes/links.d.ts +8 -0
- package/dist/routes/links.js +18 -0
- package/dist/routes/media-public.d.ts +11 -0
- package/dist/routes/media-public.js +125 -0
- package/dist/routes/media.d.ts +9 -0
- package/dist/routes/media.js +80 -0
- package/dist/routes/menus.d.ts +8 -0
- package/dist/routes/menus.js +148 -0
- package/dist/routes/preview-revision.d.ts +10 -0
- package/dist/routes/preview-revision.js +96 -0
- package/dist/routes/preview.d.ts +10 -0
- package/dist/routes/preview.js +56 -0
- package/dist/routes/public.d.ts +10 -0
- package/dist/routes/public.js +123 -0
- package/dist/routes/publish.d.ts +10 -0
- package/dist/routes/publish.js +139 -0
- package/dist/routes/redirects.d.ts +8 -0
- package/dist/routes/redirects.js +132 -0
- package/dist/routes/search-public.d.ts +6 -0
- package/dist/routes/search-public.js +104 -0
- package/dist/routes/search.d.ts +8 -0
- package/dist/routes/search.js +15 -0
- package/dist/routes/settings.d.ts +10 -0
- package/dist/routes/settings.js +68 -0
- package/dist/routes/sitemap.d.ts +7 -0
- package/dist/routes/sitemap.js +79 -0
- package/dist/routes/theme.d.ts +10 -0
- package/dist/routes/theme.js +116 -0
- package/dist/schemas/instance.schema.json +17 -0
- package/dist/schemas/menu.schema.json +24 -0
- package/dist/schemas/page.schema.json +26 -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 +7 -0
- package/dist/search/drivers/node-sqlite-driver.js +19 -0
- package/dist/search/query-content.d.ts +32 -0
- package/dist/search/query-content.js +207 -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 +3 -0
- package/dist/search/rebuild-index.js +320 -0
- package/dist/server-config.d.ts +23 -0
- package/dist/server-config.js +237 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +235 -0
- package/dist/services/batch.d.ts +35 -0
- package/dist/services/batch.js +145 -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 +151 -0
- package/dist/services/delete-content.d.ts +12 -0
- package/dist/services/delete-content.js +166 -0
- package/dist/services/deleted-pages.d.ts +11 -0
- package/dist/services/deleted-pages.js +83 -0
- package/dist/services/dev-tunnel.d.ts +5 -0
- package/dist/services/dev-tunnel.js +5 -0
- package/dist/services/drafts.d.ts +16 -0
- package/dist/services/drafts.js +164 -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/links.d.ts +22 -0
- package/dist/services/links.js +169 -0
- package/dist/services/manage-menus.d.ts +15 -0
- package/dist/services/manage-menus.js +138 -0
- package/dist/services/manage-redirects.d.ts +25 -0
- package/dist/services/manage-redirects.js +156 -0
- package/dist/services/menu-references.d.ts +3 -0
- package/dist/services/menu-references.js +38 -0
- package/dist/services/menus.d.ts +9 -0
- package/dist/services/menus.js +43 -0
- package/dist/services/migration-runner.d.ts +16 -0
- package/dist/services/migration-runner.js +169 -0
- package/dist/services/mime-types.d.ts +3 -0
- package/dist/services/mime-types.js +43 -0
- package/dist/services/move.d.ts +16 -0
- package/dist/services/move.js +236 -0
- package/dist/services/path-safety.d.ts +6 -0
- package/dist/services/path-safety.js +112 -0
- package/dist/services/pid-file.d.ts +13 -0
- package/dist/services/pid-file.js +54 -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 +15 -0
- package/dist/services/publish.js +313 -0
- package/dist/services/rate-limit-config.d.ts +9 -0
- package/dist/services/rate-limit-config.js +22 -0
- package/dist/services/redirects.d.ts +27 -0
- package/dist/services/redirects.js +175 -0
- package/dist/services/reindex-on-write.d.ts +3 -0
- package/dist/services/reindex-on-write.js +30 -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/site-settings.d.ts +28 -0
- package/dist/services/site-settings.js +116 -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 +6 -0
- package/dist/services/theme-component-file.js +39 -0
- package/dist/services/theme-files.d.ts +33 -0
- package/dist/services/theme-files.js +148 -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 +137 -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 +26 -0
- package/dist/services/validation.js +195 -0
- package/dist/services/write-queue.d.ts +1 -0
- package/dist/services/write-queue.js +14 -0
- package/dist/site-check/cli.d.ts +2 -0
- package/dist/site-check/cli.js +48 -0
- package/dist/site-check/run-check.d.ts +12 -0
- package/dist/site-check/run-check.js +188 -0
- package/dist/site-sync/cli-helpers.d.ts +23 -0
- package/dist/site-sync/cli-helpers.js +75 -0
- package/dist/site-sync/deploy.d.ts +36 -0
- package/dist/site-sync/deploy.js +124 -0
- package/dist/site-sync/live-content.d.ts +30 -0
- package/dist/site-sync/live-content.js +68 -0
- package/dist/site-sync/prompts.d.ts +30 -0
- package/dist/site-sync/prompts.js +237 -0
- package/dist/site-sync/pull-cli.d.ts +2 -0
- package/dist/site-sync/pull-cli.js +75 -0
- package/dist/site-sync/pull-site.d.ts +28 -0
- package/dist/site-sync/pull-site.js +171 -0
- package/dist/site-sync/push-cli.d.ts +2 -0
- package/dist/site-sync/push-cli.js +268 -0
- package/dist/site-sync/push-site.d.ts +96 -0
- package/dist/site-sync/push-site.js +376 -0
- package/dist/site-sync/remote-site.d.ts +38 -0
- package/dist/site-sync/remote-site.js +122 -0
- package/dist/site-sync/sync-record.d.ts +26 -0
- package/dist/site-sync/sync-record.js +76 -0
- package/dist/site-sync/theme-sync.d.ts +34 -0
- package/dist/site-sync/theme-sync.js +128 -0
- package/dist/stop-site/cli.d.ts +2 -0
- package/dist/stop-site/cli.js +28 -0
- package/dist/stop-site/stop-site.d.ts +26 -0
- package/dist/stop-site/stop-site.js +56 -0
- package/dist/theme-state.d.ts +20 -0
- package/dist/theme-state.js +33 -0
- package/dist/upgrade/changelog.d.ts +8 -0
- package/dist/upgrade/changelog.js +46 -0
- package/dist/upgrade/finish.d.ts +9 -0
- package/dist/upgrade/finish.js +30 -0
- package/dist/upgrade/owned-files.d.ts +11 -0
- package/dist/upgrade/owned-files.js +42 -0
- package/dist/upgrade/package-names.d.ts +4 -0
- package/dist/upgrade/package-names.js +26 -0
- package/dist/upgrade/upgrade-cli.d.ts +2 -0
- package/dist/upgrade/upgrade-cli.js +170 -0
- package/package.json +64 -4
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { CURRENT_SCHEMA_VERSION } from "../migrations/index.js";
|
|
4
|
+
import { NO_AUTH_ROUTE_RATE_LIMIT } from "../services/rate-limit-config.js";
|
|
5
|
+
import { DRIVER_NAME } from "../search/drivers/node-sqlite-driver.js";
|
|
6
|
+
// The agent's own bundled package.json, not site data - the same
|
|
7
|
+
// import.meta.dirname-relative pattern already established in
|
|
8
|
+
// validation.ts/startup-checks.ts for the agent's own files.
|
|
9
|
+
export function readAgentVersion() {
|
|
10
|
+
const packageJsonPath = join(import.meta.dirname, '..', '..', 'package.json');
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
12
|
+
return packageJson.version ?? '0.0.0';
|
|
13
|
+
}
|
|
14
|
+
export const capabilitiesRoutes = async (fastify, opts) => {
|
|
15
|
+
fastify.get('/capabilities', { config: NO_AUTH_ROUTE_RATE_LIMIT }, async () => ({
|
|
16
|
+
agentVersion: readAgentVersion(),
|
|
17
|
+
contentSchemaVersion: CURRENT_SCHEMA_VERSION,
|
|
18
|
+
sqliteDriver: DRIVER_NAME,
|
|
19
|
+
// Named distinctly from site.config.json's own "media.maxUploadBytes"
|
|
20
|
+
// key so this response reads unambiguously next to its siblings -
|
|
21
|
+
// lets the admin's own client-side upload check validate against
|
|
22
|
+
// the same limit the server will actually enforce (@fastify/multipart's
|
|
23
|
+
// limits.fileSize in media.ts), rather than a separately hardcoded
|
|
24
|
+
// guess that could drift.
|
|
25
|
+
maxMediaUploadBytes: opts.maxUploadBytes,
|
|
26
|
+
}));
|
|
27
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
2
|
+
import type { SiteConfig } from '../config.ts';
|
|
3
|
+
import type { TokenEntry } from '../server-config.ts';
|
|
4
|
+
export interface ContentRouteOptions {
|
|
5
|
+
config: SiteConfig;
|
|
6
|
+
tokens: TokenEntry[];
|
|
7
|
+
}
|
|
8
|
+
export declare const contentRoutes: FastifyPluginAsync<ContentRouteOptions>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ContentReadError, listContent, readContentFile } from "../services/content-read.js";
|
|
2
|
+
import { DeleteContentError, deleteContent } from "../services/delete-content.js";
|
|
3
|
+
import { isValidCommitAuthor } from "../services/git.js";
|
|
4
|
+
import { MoveError, movePage } from "../services/move.js";
|
|
5
|
+
import { PathSafetyError } from "../services/path-safety.js";
|
|
6
|
+
import { WRITE_ROUTE_RATE_LIMIT } from "../services/rate-limit-config.js";
|
|
7
|
+
import { requireScope } from "../services/token-auth.js";
|
|
8
|
+
function isNonEmptyString(value) {
|
|
9
|
+
return typeof value === 'string' && value.length > 0;
|
|
10
|
+
}
|
|
11
|
+
// content/drafts/ and content/redirects.json sit inside contentRoot
|
|
12
|
+
// (Group N) but are not page-shaped content reachable through the
|
|
13
|
+
// generic content routes - drafts have their own dedicated /v1/drafts/*
|
|
14
|
+
// surface with its own semantics, and redirects.json is agent-managed
|
|
15
|
+
// data with its own /v1/redirects surface. Without this check, a
|
|
16
|
+
// client request resolving into either would either leak an unpublished
|
|
17
|
+
// draft's raw bytes through the "live content" read endpoint, or
|
|
18
|
+
// (for DELETE) unlink a file that was never git-tracked and blow up in
|
|
19
|
+
// commitPaths with a spurious 500 - both real bugs the nesting would
|
|
20
|
+
// introduce, not present when drafts/redirects.json were siteRoot
|
|
21
|
+
// siblings of contentRoot.
|
|
22
|
+
function isReservedContentPath(relativePath) {
|
|
23
|
+
return (relativePath === 'redirects.json' ||
|
|
24
|
+
relativePath === 'settings.json' ||
|
|
25
|
+
relativePath === 'drafts' ||
|
|
26
|
+
relativePath.startsWith('drafts/'));
|
|
27
|
+
}
|
|
28
|
+
async function handleReadContent(request, reply, config) {
|
|
29
|
+
const relativePath = request.params['*'];
|
|
30
|
+
if (isReservedContentPath(relativePath)) {
|
|
31
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: `No content at "${relativePath}"` });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const { bytes, etag } = readContentFile(config.contentRoot, relativePath);
|
|
36
|
+
reply.header('etag', etag).type('application/json; charset=utf-8').send(bytes);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
// PathSafetyError has no .statusCode - left uncaught it would fall
|
|
40
|
+
// through to the global handler's generic sanitised 500, matching
|
|
41
|
+
// the gap preview.ts's precedent already guards against.
|
|
42
|
+
if (error instanceof PathSafetyError || (error instanceof ContentReadError && error.reason === 'not-found')) {
|
|
43
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: `No content at "${relativePath}"` });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function parseDeleteContentBody(body) {
|
|
50
|
+
if (typeof body !== 'object' || body === null) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const { redirectTo, message, author } = body;
|
|
54
|
+
if (redirectTo !== undefined && !isNonEmptyString(redirectTo)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (!isNonEmptyString(message) || !isValidCommitAuthor(author)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return { redirectTo, message, author };
|
|
61
|
+
}
|
|
62
|
+
async function handleDeleteContent(request, reply, config) {
|
|
63
|
+
const relativePath = request.params['*'];
|
|
64
|
+
if (isReservedContentPath(relativePath)) {
|
|
65
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: `No content at "${relativePath}"` });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const parsed = parseDeleteContentBody(request.body);
|
|
69
|
+
if (!parsed) {
|
|
70
|
+
reply.code(400).send({
|
|
71
|
+
statusCode: 400,
|
|
72
|
+
error: 'Bad Request',
|
|
73
|
+
message: 'Expected { redirectTo?: string, message: string, author: { name, email } }',
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await deleteContent(config, relativePath, parsed.redirectTo, parsed.message, parsed.author);
|
|
79
|
+
reply.code(204).send();
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof PathSafetyError) {
|
|
83
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: `No content at "${relativePath}"` });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (error instanceof DeleteContentError) {
|
|
87
|
+
if (error.reason === 'page-not-found') {
|
|
88
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: error.message });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (error.reason === 'has-children') {
|
|
92
|
+
reply.code(409).send({ statusCode: 409, error: 'Conflict', message: error.message });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (error.reason === 'redirect-cycle' || error.reason === 'invalid-redirect-target') {
|
|
96
|
+
reply.code(400).send({ statusCode: 400, error: 'Bad Request', message: error.message });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function parseMoveContentBody(body) {
|
|
104
|
+
if (typeof body !== 'object' || body === null) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const { from, to, message, author, createRedirect } = body;
|
|
108
|
+
if (!isNonEmptyString(from) || !isNonEmptyString(to)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (!isNonEmptyString(message) || !isValidCommitAuthor(author)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (createRedirect !== undefined && typeof createRedirect !== 'boolean') {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return { from, to, message, author, createRedirect };
|
|
118
|
+
}
|
|
119
|
+
async function handleMoveContent(request, reply, config) {
|
|
120
|
+
const parsed = parseMoveContentBody(request.body);
|
|
121
|
+
if (!parsed) {
|
|
122
|
+
reply.code(400).send({
|
|
123
|
+
statusCode: 400,
|
|
124
|
+
error: 'Bad Request',
|
|
125
|
+
message: 'Expected { from: string, to: string, message: string, author: { name, email } }',
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await movePage(config, parsed.from, parsed.to, parsed.message, parsed.author, {
|
|
131
|
+
createRedirect: parsed.createRedirect,
|
|
132
|
+
});
|
|
133
|
+
reply.send({ ok: true });
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof PathSafetyError) {
|
|
137
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: `No page at "${parsed.from}"` });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (error instanceof MoveError) {
|
|
141
|
+
if (error.reason === 'source-not-found') {
|
|
142
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: error.message });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (error.reason === 'destination-exists') {
|
|
146
|
+
reply.code(409).send({ statusCode: 409, error: 'Conflict', message: error.message });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function handleListContent(request, reply, config) {
|
|
154
|
+
const { type, prefix, draftStatus } = request.query;
|
|
155
|
+
const entries = listContent(config, { type, prefix, draftStatus });
|
|
156
|
+
reply.send(entries);
|
|
157
|
+
}
|
|
158
|
+
export const contentRoutes = async (fastify, opts) => {
|
|
159
|
+
fastify.get('/content', { preHandler: requireScope(opts.tokens, 'content') }, async (request, reply) => handleListContent(request, reply, opts.config));
|
|
160
|
+
fastify.get('/content/*', { preHandler: requireScope(opts.tokens, 'content') }, async (request, reply) => handleReadContent(request, reply, opts.config));
|
|
161
|
+
fastify.delete('/content/*', { preHandler: requireScope(opts.tokens, 'content'), config: WRITE_ROUTE_RATE_LIMIT }, async (request, reply) => handleDeleteContent(request, reply, opts.config));
|
|
162
|
+
// A static path, distinct from the GET/DELETE /content/* wildcard
|
|
163
|
+
// above by method as well as by not matching the wildcard's own
|
|
164
|
+
// prefix shape at all (POST here, GET/DELETE there) - no routing
|
|
165
|
+
// collision, per Group C's own verified precedent that Fastify
|
|
166
|
+
// prefers static/exact matches over a wildcard regardless of
|
|
167
|
+
// registration order.
|
|
168
|
+
fastify.post('/content/move', { preHandler: requireScope(opts.tokens, 'content'), config: WRITE_ROUTE_RATE_LIMIT }, async (request, reply) => handleMoveContent(request, reply, opts.config));
|
|
169
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
2
|
+
import type { SiteConfig } from '../config.ts';
|
|
3
|
+
import type { TokenEntry } from '../server-config.ts';
|
|
4
|
+
export interface DeletedPagesRouteOptions {
|
|
5
|
+
config: SiteConfig;
|
|
6
|
+
tokens: TokenEntry[];
|
|
7
|
+
}
|
|
8
|
+
export declare const deletedPagesRoutes: FastifyPluginAsync<DeletedPagesRouteOptions>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { listDeletedPages } from "../services/deleted-pages.js";
|
|
2
|
+
import { requireScope } from "../services/token-auth.js";
|
|
3
|
+
// GET /v1/deleted-pages: pages that were deleted and aren't back, newest
|
|
4
|
+
// first, each with the ref to restore it from (POST /v1/git/revert).
|
|
5
|
+
export const deletedPagesRoutes = async (fastify, opts) => {
|
|
6
|
+
fastify.get('/deleted-pages', { preHandler: requireScope(opts.tokens, 'content') }, async (_request, reply) => {
|
|
7
|
+
reply.send({ pages: listDeletedPages(opts.config) });
|
|
8
|
+
});
|
|
9
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
2
|
+
import type { SiteConfig } from '../config.ts';
|
|
3
|
+
import type { ThemeState } from '../theme-state.ts';
|
|
4
|
+
import type { TokenEntry } from '../server-config.ts';
|
|
5
|
+
export interface DraftsRouteOptions {
|
|
6
|
+
config: SiteConfig;
|
|
7
|
+
theme: ThemeState;
|
|
8
|
+
tokens: TokenEntry[];
|
|
9
|
+
}
|
|
10
|
+
export declare const draftsRoutes: FastifyPluginAsync<DraftsRouteOptions>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ContentReadError, readContentFile } from "../services/content-read.js";
|
|
2
|
+
import { DraftError, discardDraft, saveDraft } from "../services/drafts.js";
|
|
3
|
+
import { PathSafetyError } from "../services/path-safety.js";
|
|
4
|
+
import { WRITE_ROUTE_RATE_LIMIT } from "../services/rate-limit-config.js";
|
|
5
|
+
import { requireScope } from "../services/token-auth.js";
|
|
6
|
+
async function handleReadDraft(request, reply, config) {
|
|
7
|
+
const relativePath = request.params['*'];
|
|
8
|
+
try {
|
|
9
|
+
const { bytes, etag } = readContentFile(config.draftsRoot, relativePath);
|
|
10
|
+
reply.header('etag', etag).type('application/json; charset=utf-8').send(bytes);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error instanceof PathSafetyError || (error instanceof ContentReadError && error.reason === 'not-found')) {
|
|
14
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: `No draft at "${relativePath}"` });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function handleSaveDraft(request, reply, config, themeSchemas) {
|
|
21
|
+
const relativePath = request.params['*'];
|
|
22
|
+
// A static per-request property (is the header present at all on
|
|
23
|
+
// *this* request), not shared mutable state - no TOCTOU risk in
|
|
24
|
+
// checking it here, before the queue, unlike the ETag comparison
|
|
25
|
+
// itself (E2/E4/E6), which must happen inside saveDraftJob.
|
|
26
|
+
const ifMatch = request.headers['if-match'];
|
|
27
|
+
if (typeof ifMatch !== 'string' || ifMatch.length === 0) {
|
|
28
|
+
reply.code(428).send({
|
|
29
|
+
statusCode: 428,
|
|
30
|
+
error: 'Precondition Required',
|
|
31
|
+
message: 'An If-Match header is required to save a draft',
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const newEtag = await saveDraft(config, themeSchemas, relativePath, request.body, ifMatch);
|
|
37
|
+
reply.header('etag', newEtag).send({ ok: true });
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof PathSafetyError) {
|
|
41
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: `No draft at "${relativePath}"` });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (error instanceof DraftError && error.reason === 'conflict') {
|
|
45
|
+
reply.code(409).send({ statusCode: 409, error: 'Conflict', message: error.message });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (error instanceof DraftError && error.reason === 'validation-failed') {
|
|
49
|
+
reply.code(400).send({ statusCode: 400, error: 'Bad Request', message: error.message, errors: error.errors });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function handleDiscardDraft(request, reply, config) {
|
|
56
|
+
const relativePath = request.params['*'];
|
|
57
|
+
try {
|
|
58
|
+
await discardDraft(config, relativePath);
|
|
59
|
+
reply.code(204).send();
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error instanceof PathSafetyError) {
|
|
63
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: `No draft at "${relativePath}"` });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export const draftsRoutes = async (fastify, opts) => {
|
|
70
|
+
fastify.get('/drafts/*', { preHandler: requireScope(opts.tokens, 'content') }, async (request, reply) => handleReadDraft(request, reply, opts.config));
|
|
71
|
+
fastify.put('/drafts/*', { preHandler: requireScope(opts.tokens, 'content'), config: WRITE_ROUTE_RATE_LIMIT }, async (request, reply) => handleSaveDraft(request, reply, opts.config, opts.theme.current.themeSchemas));
|
|
72
|
+
fastify.delete('/drafts/*', { preHandler: requireScope(opts.tokens, 'content'), config: WRITE_ROUTE_RATE_LIMIT }, async (request, reply) => handleDiscardDraft(request, reply, opts.config));
|
|
73
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
2
|
+
import type { SiteConfig } from '../config.ts';
|
|
3
|
+
import type { TokenEntry } from '../server-config.ts';
|
|
4
|
+
export interface GitRouteOptions {
|
|
5
|
+
config: SiteConfig;
|
|
6
|
+
tokens: TokenEntry[];
|
|
7
|
+
}
|
|
8
|
+
export declare const gitRoutes: FastifyPluginAsync<GitRouteOptions>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { commitWorkingTreeChanges } from "../services/git-commit.js";
|
|
2
|
+
import { GitShowError, getCommitLog, readFileAtRevision } from "../services/git-history.js";
|
|
3
|
+
import { RevertError, revertPaths } from "../services/git-revert.js";
|
|
4
|
+
import { isValidCommitAuthor, isValidGitRef } from "../services/git.js";
|
|
5
|
+
import { mimeTypeFor } from "../services/mime-types.js";
|
|
6
|
+
import { PathSafetyError } from "../services/path-safety.js";
|
|
7
|
+
import { WRITE_ROUTE_RATE_LIMIT } from "../services/rate-limit-config.js";
|
|
8
|
+
import { requireScope } from "../services/token-auth.js";
|
|
9
|
+
function isNonEmptyString(value) {
|
|
10
|
+
return typeof value === 'string' && value.length > 0;
|
|
11
|
+
}
|
|
12
|
+
async function handleGitLog(request, reply, config) {
|
|
13
|
+
const { path, limit: rawLimit } = request.query;
|
|
14
|
+
let limit;
|
|
15
|
+
if (rawLimit !== undefined) {
|
|
16
|
+
const parsed = Number(rawLimit);
|
|
17
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
18
|
+
reply.code(400).send({
|
|
19
|
+
statusCode: 400,
|
|
20
|
+
error: 'Bad Request',
|
|
21
|
+
message: '"limit" must be a positive integer',
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
limit = parsed;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const result = getCommitLog(config, { path, limit });
|
|
29
|
+
reply.send(result);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
// PathSafetyError has no .statusCode - left uncaught it falls
|
|
33
|
+
// through to the global handler's sanitised 500, the same gap
|
|
34
|
+
// every other route touching a :path already guards against.
|
|
35
|
+
if (error instanceof PathSafetyError) {
|
|
36
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: 'No content at that path' });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function handleGitShow(request, reply, config) {
|
|
43
|
+
const { ref } = request.params;
|
|
44
|
+
const relativePath = request.params['*'];
|
|
45
|
+
if (!isValidGitRef(ref)) {
|
|
46
|
+
reply.code(400).send({ statusCode: 400, error: 'Bad Request', message: `Invalid ref: "${ref}"` });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const content = readFileAtRevision(config, ref, relativePath);
|
|
51
|
+
reply.type(mimeTypeFor(relativePath)).send(content);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof PathSafetyError) {
|
|
55
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: 'No content at that path' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (error instanceof GitShowError) {
|
|
59
|
+
// Two distinct codes, not collapsed to one 404: unlike public.ts's
|
|
60
|
+
// deliberate traversal-vs-miss collapse (justified there because
|
|
61
|
+
// that route is unauthenticated, where leaking which case
|
|
62
|
+
// occurred would aid enumeration), this route sits behind
|
|
63
|
+
// requireScope already - there's no equivalent risk, and
|
|
64
|
+
// collapsing here would only make an admin's diff/history UI
|
|
65
|
+
// worse for no security benefit.
|
|
66
|
+
const code = error.reason === 'invalid-ref' ? 400 : 404;
|
|
67
|
+
reply.code(code).send({
|
|
68
|
+
statusCode: code,
|
|
69
|
+
error: code === 400 ? 'Bad Request' : 'Not Found',
|
|
70
|
+
message: error.message,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function parseRevertBody(body) {
|
|
78
|
+
if (typeof body !== 'object' || body === null) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const { ref, paths, message, author } = body;
|
|
82
|
+
if (!isNonEmptyString(ref)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(paths) || paths.length === 0 || !paths.every(isNonEmptyString)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (!isNonEmptyString(message) || !isValidCommitAuthor(author)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return { ref, paths, message, author };
|
|
92
|
+
}
|
|
93
|
+
async function handleGitRevert(request, reply, config) {
|
|
94
|
+
const parsed = parseRevertBody(request.body);
|
|
95
|
+
if (!parsed) {
|
|
96
|
+
reply.code(400).send({
|
|
97
|
+
statusCode: 400,
|
|
98
|
+
error: 'Bad Request',
|
|
99
|
+
message: 'Expected { ref: string, paths: string[], message: string, author: { name, email } }',
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
await revertPaths(config, parsed.ref, parsed.paths, parsed.message, parsed.author);
|
|
105
|
+
reply.send({ ok: true });
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error instanceof PathSafetyError) {
|
|
109
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: 'No content at that path' });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (error instanceof RevertError) {
|
|
113
|
+
if (error.reason === 'invalid-ref') {
|
|
114
|
+
reply.code(400).send({ statusCode: 400, error: 'Bad Request', message: error.message });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (error.reason === 'path-not-found-at-ref') {
|
|
118
|
+
reply.code(404).send({ statusCode: 404, error: 'Not Found', message: error.message });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// write-failed/commit-failed/rollback-failed: no client-side fix
|
|
122
|
+
// applies, fall through to the global handler's sanitised 500.
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function parseCommitBody(body) {
|
|
128
|
+
if (typeof body !== 'object' || body === null) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const { message, author } = body;
|
|
132
|
+
if (!isNonEmptyString(message) || !isValidCommitAuthor(author)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return { message, author };
|
|
136
|
+
}
|
|
137
|
+
async function handleGitCommit(request, reply, config) {
|
|
138
|
+
const parsed = parseCommitBody(request.body);
|
|
139
|
+
if (!parsed) {
|
|
140
|
+
reply.code(400).send({
|
|
141
|
+
statusCode: 400,
|
|
142
|
+
error: 'Bad Request',
|
|
143
|
+
message: 'Expected { message: string, author: { name, email } }',
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Any GitOperationError here is not caught - it rethrows to the
|
|
148
|
+
// global error handler's sanitised 500, same as every other route's
|
|
149
|
+
// uncaught commitPaths failure.
|
|
150
|
+
const result = await commitWorkingTreeChanges(config, parsed.message, parsed.author);
|
|
151
|
+
reply.send({ ok: true, committed: result === 'committed' });
|
|
152
|
+
}
|
|
153
|
+
export const gitRoutes = async (fastify, opts) => {
|
|
154
|
+
fastify.get('/git/log', { preHandler: requireScope(opts.tokens, 'content') }, async (request, reply) => handleGitLog(request, reply, opts.config));
|
|
155
|
+
fastify.get('/git/show/:ref/*', { preHandler: requireScope(opts.tokens, 'content') }, async (request, reply) => handleGitShow(request, reply, opts.config));
|
|
156
|
+
fastify.post('/git/revert', { preHandler: requireScope(opts.tokens, 'content'), config: WRITE_ROUTE_RATE_LIMIT }, async (request, reply) => handleGitRevert(request, reply, opts.config));
|
|
157
|
+
fastify.post('/git/commit', { preHandler: requireScope(opts.tokens, 'content'), config: WRITE_ROUTE_RATE_LIMIT }, async (request, reply) => handleGitCommit(request, reply, opts.config));
|
|
158
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
2
|
+
import type { SiteConfig } from '../config.ts';
|
|
3
|
+
import type { TokenEntry } from '../server-config.ts';
|
|
4
|
+
import type { ThemeState } from '../theme-state.ts';
|
|
5
|
+
export interface V1RouteOptions {
|
|
6
|
+
config: SiteConfig;
|
|
7
|
+
theme: ThemeState;
|
|
8
|
+
tokens: TokenEntry[];
|
|
9
|
+
ipAllowlist: string[];
|
|
10
|
+
maxUploadBytes: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const v1Routes: FastifyPluginAsync<V1RouteOptions>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { batchRoutes } from "./batch.js";
|
|
2
|
+
import { capabilitiesRoutes } from "./capabilities.js";
|
|
3
|
+
import { contentRoutes } from "./content.js";
|
|
4
|
+
import { draftsRoutes } from "./drafts.js";
|
|
5
|
+
import { gitRoutes } from "./git.js";
|
|
6
|
+
import { mediaRoutes } from "./media.js";
|
|
7
|
+
import { menusRoutes } from "./menus.js";
|
|
8
|
+
import { previewRevisionRoutes } from "./preview-revision.js";
|
|
9
|
+
import { previewRoutes } from "./preview.js";
|
|
10
|
+
import { publishRoutes } from "./publish.js";
|
|
11
|
+
import { redirectsRoutes } from "./redirects.js";
|
|
12
|
+
import { searchRoutes } from "./search.js";
|
|
13
|
+
import { deletedPagesRoutes } from "./deleted-pages.js";
|
|
14
|
+
import { linksRoutes } from "./links.js";
|
|
15
|
+
import { settingsRoutes } from "./settings.js";
|
|
16
|
+
import { themeRoutes } from "./theme.js";
|
|
17
|
+
import { ipAllowlistGuard } from "../services/ip-allowlist.js";
|
|
18
|
+
// The /v1 aggregator: buildServer registers this once with
|
|
19
|
+
// { prefix: '/v1', ...V1RouteOptions }, and every route-group plugin
|
|
20
|
+
// (capabilities.ts, preview.ts, and later content/drafts/publish/etc.)
|
|
21
|
+
// registers itself here with a bare, unprefixed path. Each route-group
|
|
22
|
+
// plugin is registered plainly (never wrapped in fastify-plugin's
|
|
23
|
+
// fp()), so it keeps its own encapsulation scope - required for Group
|
|
24
|
+
// B's auth preHandler hooks to gate only the routes that need them.
|
|
25
|
+
// Plugins that don't need site data (capabilities.ts) simply ignore
|
|
26
|
+
// the passed-through opts.
|
|
27
|
+
export const v1Routes = async (fastify, opts) => {
|
|
28
|
+
// A coarse "is this client's network address allowed to talk to the
|
|
29
|
+
// API at all" gate, applied to the entire /v1 surface uniformly
|
|
30
|
+
// (including capabilities.ts and every GET route) - orthogonal to
|
|
31
|
+
// requireScope's per-route, per-scope exemptions. Added on this
|
|
32
|
+
// fastify instance (v1Routes's own parameter, never wrapped in fp())
|
|
33
|
+
// before any nested register() call, so it cascades to every plugin
|
|
34
|
+
// registered as a child below but never to publicRoutes/assetsRoutes
|
|
35
|
+
// (siblings registered directly on the root app, outside this
|
|
36
|
+
// encapsulation context) - verified with a real request, not just
|
|
37
|
+
// asserted.
|
|
38
|
+
fastify.addHook('onRequest', ipAllowlistGuard(opts.ipAllowlist));
|
|
39
|
+
fastify.register(capabilitiesRoutes, { maxUploadBytes: opts.maxUploadBytes });
|
|
40
|
+
// Fastify passes the *entire* options object a plugin was registered
|
|
41
|
+
// with - including `prefix` - through as this plugin's own second
|
|
42
|
+
// argument. Passing `opts` straight through to a nested register()
|
|
43
|
+
// call would carry that `prefix: '/v1'` along with it, double-
|
|
44
|
+
// applying the prefix (confirmed empirically: routes ended up
|
|
45
|
+
// registered at /v1/v1/preview/*). Re-pack only the site-data
|
|
46
|
+
// fields, never `opts` itself, for any nested plugin registration.
|
|
47
|
+
fastify.register(previewRoutes, {
|
|
48
|
+
config: opts.config,
|
|
49
|
+
theme: opts.theme,
|
|
50
|
+
tokens: opts.tokens,
|
|
51
|
+
});
|
|
52
|
+
fastify.register(previewRevisionRoutes, {
|
|
53
|
+
config: opts.config,
|
|
54
|
+
theme: opts.theme,
|
|
55
|
+
tokens: opts.tokens,
|
|
56
|
+
});
|
|
57
|
+
// contentRoutes needs no themeTemplates (it never renders anything),
|
|
58
|
+
// so it gets its own narrower options shape rather than the full
|
|
59
|
+
// bundle out of convenience - matching capabilities.ts's precedent
|
|
60
|
+
// of registering with only what it actually uses.
|
|
61
|
+
fastify.register(contentRoutes, { config: opts.config, tokens: opts.tokens });
|
|
62
|
+
fastify.register(draftsRoutes, {
|
|
63
|
+
config: opts.config,
|
|
64
|
+
theme: opts.theme,
|
|
65
|
+
tokens: opts.tokens,
|
|
66
|
+
});
|
|
67
|
+
fastify.register(publishRoutes, {
|
|
68
|
+
config: opts.config,
|
|
69
|
+
theme: opts.theme,
|
|
70
|
+
tokens: opts.tokens,
|
|
71
|
+
});
|
|
72
|
+
fastify.register(batchRoutes, {
|
|
73
|
+
config: opts.config,
|
|
74
|
+
theme: opts.theme,
|
|
75
|
+
tokens: opts.tokens,
|
|
76
|
+
});
|
|
77
|
+
fastify.register(gitRoutes, { config: opts.config, tokens: opts.tokens });
|
|
78
|
+
fastify.register(mediaRoutes, { config: opts.config, tokens: opts.tokens, maxUploadBytes: opts.maxUploadBytes });
|
|
79
|
+
fastify.register(searchRoutes, { config: opts.config, tokens: opts.tokens });
|
|
80
|
+
fastify.register(redirectsRoutes, { config: opts.config, tokens: opts.tokens });
|
|
81
|
+
fastify.register(menusRoutes, { config: opts.config, tokens: opts.tokens });
|
|
82
|
+
fastify.register(themeRoutes, { config: opts.config, theme: opts.theme, tokens: opts.tokens });
|
|
83
|
+
fastify.register(settingsRoutes, { config: opts.config, theme: opts.theme, tokens: opts.tokens });
|
|
84
|
+
fastify.register(linksRoutes, { config: opts.config, tokens: opts.tokens });
|
|
85
|
+
fastify.register(deletedPagesRoutes, { config: opts.config, tokens: opts.tokens });
|
|
86
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
2
|
+
import type { SiteConfig } from '../config.ts';
|
|
3
|
+
import type { TokenEntry } from '../server-config.ts';
|
|
4
|
+
export interface LinksRouteOptions {
|
|
5
|
+
config: SiteConfig;
|
|
6
|
+
tokens: TokenEntry[];
|
|
7
|
+
}
|
|
8
|
+
export declare const linksRoutes: FastifyPluginAsync<LinksRouteOptions>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { findReferences, linkTarget } from "../services/links.js";
|
|
2
|
+
import { requireScope } from "../services/token-auth.js";
|
|
3
|
+
// GET /v1/links?to=/about: every page, draft, menu and the site settings
|
|
4
|
+
// that link to that page - what the admin lists before a page is
|
|
5
|
+
// deleted. `to` is only ever compared against links found in content,
|
|
6
|
+
// never used as a file path. Found by reading the content each time
|
|
7
|
+
// rather than kept in an index: always current, and cheap at a site's
|
|
8
|
+
// size.
|
|
9
|
+
export const linksRoutes = async (fastify, opts) => {
|
|
10
|
+
fastify.get('/links', { preHandler: requireScope(opts.tokens, 'content') }, async (request, reply) => {
|
|
11
|
+
const to = request.query.to;
|
|
12
|
+
if (typeof to !== 'string' || linkTarget(to) === null) {
|
|
13
|
+
reply.code(400).send({ statusCode: 400, error: 'Bad Request', message: 'Expected ?to=/a-page-path' });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
reply.send({ to: linkTarget(to), references: findReferences(opts.config, to) });
|
|
17
|
+
});
|
|
18
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
2
|
+
import type { SiteConfig } from '../config.ts';
|
|
3
|
+
type ResolvedRange = {
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
} | null | 'unsatisfiable';
|
|
7
|
+
export declare function resolveRange(header: string | undefined, size: number): ResolvedRange;
|
|
8
|
+
export declare const mediaPublicRoutes: FastifyPluginAsync<{
|
|
9
|
+
config: SiteConfig;
|
|
10
|
+
}>;
|
|
11
|
+
export {};
|