@cogenta/cli 0.2.2 → 0.3.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/dist/admin-assets/assets/index-Buwdj1V2.js +71 -0
- package/dist/admin-assets/assets/index-Csef0SiA.css +1 -0
- package/dist/admin-assets/index.html +2 -2
- package/dist/commands/assistant.d.ts +65 -0
- package/dist/commands/assistant.d.ts.map +1 -0
- package/dist/commands/assistant.js +207 -0
- package/dist/commands/assistant.js.map +1 -0
- package/dist/commands/content-webhooks.d.ts +41 -0
- package/dist/commands/content-webhooks.d.ts.map +1 -0
- package/dist/commands/content-webhooks.js +61 -0
- package/dist/commands/content-webhooks.js.map +1 -0
- package/dist/commands/http-security.d.ts +39 -0
- package/dist/commands/http-security.d.ts.map +1 -0
- package/dist/commands/http-security.js +153 -0
- package/dist/commands/http-security.js.map +1 -0
- package/dist/commands/links.d.ts +24 -0
- package/dist/commands/links.d.ts.map +1 -0
- package/dist/commands/links.js +109 -0
- package/dist/commands/links.js.map +1 -0
- package/dist/commands/media-images.d.ts +67 -0
- package/dist/commands/media-images.d.ts.map +1 -0
- package/dist/commands/media-images.js +107 -0
- package/dist/commands/media-images.js.map +1 -0
- package/dist/commands/search-page.d.ts +40 -0
- package/dist/commands/search-page.d.ts.map +1 -0
- package/dist/commands/search-page.js +104 -0
- package/dist/commands/search-page.js.map +1 -0
- package/dist/commands/security-alerts.d.ts +24 -0
- package/dist/commands/security-alerts.d.ts.map +1 -0
- package/dist/commands/security-alerts.js +82 -0
- package/dist/commands/security-alerts.js.map +1 -0
- package/dist/commands/seo.d.ts +88 -0
- package/dist/commands/seo.d.ts.map +1 -0
- package/dist/commands/seo.js +155 -0
- package/dist/commands/seo.js.map +1 -0
- package/dist/commands/serve.d.ts +87 -4
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +740 -40
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/site-plan.d.ts +86 -0
- package/dist/commands/site-plan.d.ts.map +1 -0
- package/dist/commands/site-plan.js +235 -0
- package/dist/commands/site-plan.js.map +1 -0
- package/dist/commands/theme-css.d.ts +54 -0
- package/dist/commands/theme-css.d.ts.map +1 -0
- package/dist/commands/theme-css.js +121 -0
- package/dist/commands/theme-css.js.map +1 -0
- package/dist/commands/theme-render.d.ts +78 -4
- package/dist/commands/theme-render.d.ts.map +1 -1
- package/dist/commands/theme-render.js +170 -16
- package/dist/commands/theme-render.js.map +1 -1
- package/dist/commands/users.d.ts +7 -1
- package/dist/commands/users.d.ts.map +1 -1
- package/dist/commands/users.js +159 -15
- package/dist/commands/users.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -1
- package/package.json +12 -10
- package/dist/admin-assets/assets/index-21ZcDkDC.css +0 -1
- package/dist/admin-assets/assets/index-BXVsXHD2.js +0 -23
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-page.js","sourceRoot":"","sources":["../../src/commands/search-page.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RateLimiter } from '@cogenta/auth';
|
|
2
|
+
import type { Logger } from '@cogenta/core';
|
|
3
|
+
export interface SecurityAlertOptions {
|
|
4
|
+
readonly rateLimit: RateLimiter;
|
|
5
|
+
/** Where the alert is delivered. `null` disables the outbound half entirely. */
|
|
6
|
+
readonly send: ((event: string, data: Readonly<Record<string, unknown>>) => Promise<void>) | null;
|
|
7
|
+
/** Absolute base URL of the site, for the alert's required admin link. */
|
|
8
|
+
readonly siteUrl: string;
|
|
9
|
+
readonly logger: Logger;
|
|
10
|
+
/** Attempts inside the limiter's window past which an alert is worth sending. */
|
|
11
|
+
readonly minAttempts?: number;
|
|
12
|
+
readonly cooldownMs?: number;
|
|
13
|
+
readonly now?: () => number;
|
|
14
|
+
}
|
|
15
|
+
export interface SecurityAlertWatch {
|
|
16
|
+
/**
|
|
17
|
+
* Called after every `/api/auth/*` response. Cheap and silent for the
|
|
18
|
+
* overwhelming majority — a successful sign-in never touches the database
|
|
19
|
+
* here at all.
|
|
20
|
+
*/
|
|
21
|
+
observe(status: number): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare function createSecurityAlertWatch(options: SecurityAlertOptions): SecurityAlertWatch;
|
|
24
|
+
//# sourceMappingURL=security-alerts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-alerts.d.ts","sourceRoot":"","sources":["../../src/commands/security-alerts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAuB,WAAW,EAAE,MAAM,eAAe,CAAA;AAErE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AA2B3C,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAA;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAA;IACjG,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,iFAAiF;IACjF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvC;AAcD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,oBAAoB,GAAG,kBAAkB,CAuD1F"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { buildAlert } from '@cogenta/channels';
|
|
2
|
+
/**
|
|
3
|
+
* The outbound half of L14 task 4 — "exposer ça comme alerte dans le
|
|
4
|
+
* dashboard/canaux, réutilisant `@cogenta/channels`".
|
|
5
|
+
*
|
|
6
|
+
* The dashboard half is `createSuspiciousActivitySource` in `@cogenta/api`.
|
|
7
|
+
* This is the half that leaves the site, and it deliberately reuses two things
|
|
8
|
+
* that already exist rather than inventing a third: `buildAlert` (L6, with its
|
|
9
|
+
* enforced title/severity/context/expected-action/admin-link shape and its
|
|
10
|
+
* screen budget) and the signed webhook sender L14 task 1 just connected. No
|
|
11
|
+
* second notification path, no second signature.
|
|
12
|
+
*
|
|
13
|
+
* **Why the trigger is a failed request, not a timer.** Rule R1 guarantees no
|
|
14
|
+
* durable worker, so anything that "checks periodically" here would be a
|
|
15
|
+
* promise the deployment cannot keep. A brute-force run *is* a stream of
|
|
16
|
+
* requests, so the requests themselves are the clock — which also means the
|
|
17
|
+
* alert arrives while the attack is happening rather than up to a poll
|
|
18
|
+
* interval later.
|
|
19
|
+
*/
|
|
20
|
+
/** Statuses that mean a sign-in attempt was refused. 429 is the limiter itself. */
|
|
21
|
+
const REFUSED_STATUSES = new Set([401, 429]);
|
|
22
|
+
/** At most one alert per this long, however many attempts arrive. */
|
|
23
|
+
const DEFAULT_COOLDOWN_MS = 5 * 60 * 1000;
|
|
24
|
+
function summarise(summaries) {
|
|
25
|
+
return {
|
|
26
|
+
attempts: summaries.reduce((total, summary) => total + summary.attempts, 0),
|
|
27
|
+
subjects: summaries.length,
|
|
28
|
+
blocked: summaries.filter((summary) => summary.blocked).length,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function createSecurityAlertWatch(options) {
|
|
32
|
+
const now = options.now ?? Date.now;
|
|
33
|
+
const cooldownMs = options.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
34
|
+
let lastAlertAt = 0;
|
|
35
|
+
return {
|
|
36
|
+
observe: async (status) => {
|
|
37
|
+
if (options.send === null)
|
|
38
|
+
return;
|
|
39
|
+
if (!REFUSED_STATUSES.has(status))
|
|
40
|
+
return;
|
|
41
|
+
// The cooldown is checked *before* the query, not after: a script making
|
|
42
|
+
// twenty attempts a second must not turn into twenty aggregate queries a
|
|
43
|
+
// second. The alert is about a situation, not about one request.
|
|
44
|
+
if (now() - lastAlertAt < cooldownMs)
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
const summaries = await options.rateLimit.recentFailures(options.minAttempts === undefined ? undefined : { minAttempts: options.minAttempts });
|
|
48
|
+
if (summaries.length === 0)
|
|
49
|
+
return;
|
|
50
|
+
const totals = summarise(summaries);
|
|
51
|
+
lastAlertAt = now();
|
|
52
|
+
const adminUrl = new URL('/admin', options.siteUrl).toString();
|
|
53
|
+
// Counts, never the accounts — same rule as the admin notice. An
|
|
54
|
+
// outbound message travels further than a screen does, and an email
|
|
55
|
+
// address in it is an address in somebody else's inbox and logs.
|
|
56
|
+
const alert = buildAlert({
|
|
57
|
+
title: 'Repeated failed sign-in attempts',
|
|
58
|
+
severity: totals.blocked > 0 ? 'critical' : 'warning',
|
|
59
|
+
context: `${totals.attempts} failed sign-in attempts across ${totals.subjects} account(s) in the last 15 minutes${totals.blocked > 0 ? `, ${totals.blocked} of them now rate-limited` : ''}.`,
|
|
60
|
+
expectedAction: 'Check the audit log, and confirm the affected accounts still have strong passwords and a second factor.',
|
|
61
|
+
adminUrl,
|
|
62
|
+
});
|
|
63
|
+
await options.send('security.suspicious_activity', {
|
|
64
|
+
severity: alert.severity,
|
|
65
|
+
title: alert.title,
|
|
66
|
+
context: alert.context,
|
|
67
|
+
expectedAction: alert.expectedAction,
|
|
68
|
+
adminUrl: alert.adminUrl,
|
|
69
|
+
attempts: totals.attempts,
|
|
70
|
+
subjects: totals.subjects,
|
|
71
|
+
blocked: totals.blocked,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
// Never let watching a failure turn into a second failure: the sign-in
|
|
76
|
+
// response has already been written by the time this runs.
|
|
77
|
+
options.logger.error('suspicious activity alert failed', { error: String(error) });
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=security-alerts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-alerts.js","sourceRoot":"","sources":["../../src/commands/security-alerts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type AccessContext, type ContentGateway } from '@cogenta/api';
|
|
2
|
+
import type { CollectionDefinition, ContentEntry } from '@cogenta/schema';
|
|
3
|
+
import { type HreflangAlternate, type SeoImage, type SeoResource, type SeoSite, type SitemapFile } from '@cogenta/seo';
|
|
4
|
+
/**
|
|
5
|
+
* `@cogenta/seo`, actually wired to the server (L10, tasks 1-2).
|
|
6
|
+
*
|
|
7
|
+
* The package was written and tested against values, never against a running
|
|
8
|
+
* `cogenta serve` — the lot names that gap explicitly. This module is the one
|
|
9
|
+
* place the two meet: it converts what `serve.ts` holds (a `ContentGateway`,
|
|
10
|
+
* a `CollectionDefinition`, a `ContentEntry`) into what `@cogenta/seo` takes
|
|
11
|
+
* (`SeoSite`, `SeoResource`) and nothing else. No SEO decision is re-made
|
|
12
|
+
* here — every tag, every URL and every published/not-published judgement is
|
|
13
|
+
* still the package's own.
|
|
14
|
+
*/
|
|
15
|
+
export interface SiteIdentity {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly url: string;
|
|
18
|
+
readonly locales: readonly string[];
|
|
19
|
+
readonly defaultLocale: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The site as the SEO layer sees it.
|
|
23
|
+
*
|
|
24
|
+
* `unprefixedDefaultLocale` is `true` because that is what this server
|
|
25
|
+
* actually does: `createRequestListener` hands `matchPath` a `defaultLocale`,
|
|
26
|
+
* and `matchPath` then resolves `/blog/hello` as well as `/en/blog/hello`.
|
|
27
|
+
* Saying otherwise would emit canonicals that redirect to themselves — the
|
|
28
|
+
* exact failure `SeoSite`'s own doc comment warns about.
|
|
29
|
+
*/
|
|
30
|
+
export declare function seoSiteFor(site: SiteIdentity): SeoSite;
|
|
31
|
+
/**
|
|
32
|
+
* Every published entry of every routed collection, as SEO resources.
|
|
33
|
+
*
|
|
34
|
+
* Read as `ANONYMOUS` on purpose, whoever asked for the sitemap: a sitemap is
|
|
35
|
+
* a public document, and building it from the requesting actor's permissions
|
|
36
|
+
* would give a signed-in editor a different — larger — sitemap than the one a
|
|
37
|
+
* crawler fetches. `indexableResources` filters the unpublished half again on
|
|
38
|
+
* its way out, so the guarantee is doubled rather than trusted once.
|
|
39
|
+
*/
|
|
40
|
+
export declare function collectRoutedResources(collections: readonly CollectionDefinition[], gateway: ContentGateway): Promise<readonly SeoResource[]>;
|
|
41
|
+
/**
|
|
42
|
+
* The `hreflang` alternates for one entry, or an empty list.
|
|
43
|
+
*
|
|
44
|
+
* Skipped outright on a single-locale site: `alternatesFor` would return
|
|
45
|
+
* nothing anyway, and the family lookup is a filtered scan of the collection —
|
|
46
|
+
* work every page render on a monolingual site would pay for a guaranteed
|
|
47
|
+
* empty answer.
|
|
48
|
+
*
|
|
49
|
+
* The family is read through the same permission-checked gateway as the page
|
|
50
|
+
* itself, with the same actor, so a translation the caller may not see never
|
|
51
|
+
* becomes an alternate.
|
|
52
|
+
*/
|
|
53
|
+
export declare function alternatesForEntry(site: SeoSite, collection: CollectionDefinition, entry: ContentEntry, gateway: ContentGateway, context: AccessContext, locales: readonly string[]): Promise<readonly HreflangAlternate[]>;
|
|
54
|
+
export interface HeadOptions {
|
|
55
|
+
readonly alternates?: readonly HreflangAlternate[];
|
|
56
|
+
/** Resolves a media id to something a crawler can use. Absent means no social image. */
|
|
57
|
+
readonly media?: (id: string) => SeoImage | null;
|
|
58
|
+
/** Forces `noindex` — a search results page, a preview. */
|
|
59
|
+
readonly noindex?: boolean;
|
|
60
|
+
/** Overrides the derived title. Used for pages that are not one entry. */
|
|
61
|
+
readonly title?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The whole `<head>` an entry earns: title, description, canonical,
|
|
65
|
+
* `hreflang`, Open Graph, Twitter Card and a JSON-LD block.
|
|
66
|
+
*
|
|
67
|
+
* Returned as one HTML string because that is what the page template needs;
|
|
68
|
+
* every escaping decision inside it belongs to `@cogenta/seo` (`renderMetaTags`
|
|
69
|
+
* for attributes, `renderJsonLdScript` for the `</script>` hazard), never to
|
|
70
|
+
* this file.
|
|
71
|
+
*/
|
|
72
|
+
export declare function renderSeoHead(site: SeoSite, resource: SeoResource, options?: HeadOptions): string;
|
|
73
|
+
/**
|
|
74
|
+
* The sitemap files a site needs — one below the protocol limits, an index
|
|
75
|
+
* plus chunks above them.
|
|
76
|
+
*
|
|
77
|
+
* Returned as the whole set rather than one document because the split is not
|
|
78
|
+
* this layer's decision to hide: `serve.ts` routes `/sitemap.xml` *and*
|
|
79
|
+
* `/sitemap-N.xml` from the same array, so a site that grows past 50 000 URLs
|
|
80
|
+
* starts serving an index without anybody changing a route.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildSitemapFiles(site: SeoSite, resources: readonly SeoResource[]): readonly SitemapFile[];
|
|
83
|
+
export interface RobotsRenderOptions {
|
|
84
|
+
/** False on a staging or preview host: blocks every crawler outright. */
|
|
85
|
+
readonly allowIndexing?: boolean;
|
|
86
|
+
}
|
|
87
|
+
export declare function renderRobots(site: SeoSite, options?: RobotsRenderOptions): string;
|
|
88
|
+
//# sourceMappingURL=seo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../src/commands/seo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAa,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAEjF,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACzE,OAAO,EAML,KAAK,iBAAiB,EAItB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,WAAW,EAEjB,MAAM,cAAc,CAAA;AAErB;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAQtD;AAQD;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,SAAS,oBAAoB,EAAE,EAC5C,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAuCjC;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,oBAAoB,EAChC,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CA0BvC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAA;IAClD,wFAAwF;IACxF,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,QAAQ,GAAG,IAAI,CAAA;IAChD,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,WAAW,EACrB,OAAO,GAAE,WAAgB,GACxB,MAAM,CAkBR;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,SAAS,WAAW,EAAE,GAChC,SAAS,WAAW,EAAE,CAExB;AAED,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CACjC;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAUrF"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { ANONYMOUS } from '@cogenta/api';
|
|
2
|
+
import { isCogentaError } from '@cogenta/core';
|
|
3
|
+
import { alternatesFor, buildJsonLd, buildMetaTags, buildSitemap, groupTranslationFamilies, renderJsonLdScript, renderMetaTags, renderRobotsTxt, sitemapUrlsFor, } from '@cogenta/seo';
|
|
4
|
+
/**
|
|
5
|
+
* The site as the SEO layer sees it.
|
|
6
|
+
*
|
|
7
|
+
* `unprefixedDefaultLocale` is `true` because that is what this server
|
|
8
|
+
* actually does: `createRequestListener` hands `matchPath` a `defaultLocale`,
|
|
9
|
+
* and `matchPath` then resolves `/blog/hello` as well as `/en/blog/hello`.
|
|
10
|
+
* Saying otherwise would emit canonicals that redirect to themselves — the
|
|
11
|
+
* exact failure `SeoSite`'s own doc comment warns about.
|
|
12
|
+
*/
|
|
13
|
+
export function seoSiteFor(site) {
|
|
14
|
+
return {
|
|
15
|
+
baseUrl: site.url,
|
|
16
|
+
name: site.name,
|
|
17
|
+
defaultLocale: site.defaultLocale,
|
|
18
|
+
locales: site.locales,
|
|
19
|
+
unprefixedDefaultLocale: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** How many entries one collection contributes to a sitemap before it is cut off. */
|
|
23
|
+
const MAX_SITEMAP_ENTRIES_PER_COLLECTION = 5_000;
|
|
24
|
+
/** Page size used to walk a collection. The gateway's own ceiling is 100. */
|
|
25
|
+
const SCAN_PAGE = 100;
|
|
26
|
+
/**
|
|
27
|
+
* Every published entry of every routed collection, as SEO resources.
|
|
28
|
+
*
|
|
29
|
+
* Read as `ANONYMOUS` on purpose, whoever asked for the sitemap: a sitemap is
|
|
30
|
+
* a public document, and building it from the requesting actor's permissions
|
|
31
|
+
* would give a signed-in editor a different — larger — sitemap than the one a
|
|
32
|
+
* crawler fetches. `indexableResources` filters the unpublished half again on
|
|
33
|
+
* its way out, so the guarantee is doubled rather than trusted once.
|
|
34
|
+
*/
|
|
35
|
+
export async function collectRoutedResources(collections, gateway) {
|
|
36
|
+
const context = { actor: ANONYMOUS };
|
|
37
|
+
const resources = [];
|
|
38
|
+
for (const collection of collections) {
|
|
39
|
+
if (collection.routing === undefined)
|
|
40
|
+
continue;
|
|
41
|
+
// A routed collection the `public` role may not read at all makes the
|
|
42
|
+
// gateway *throw* `FORBIDDEN` rather than answer an empty page. Skipping
|
|
43
|
+
// it is the right answer — such a collection has no public URLs to list —
|
|
44
|
+
// and catching is what keeps one login-only section from turning
|
|
45
|
+
// `/sitemap.xml` into a 500 for every crawler. Found by the security
|
|
46
|
+
// review of L10 task 2.
|
|
47
|
+
try {
|
|
48
|
+
let cursor;
|
|
49
|
+
let read = 0;
|
|
50
|
+
for (;;) {
|
|
51
|
+
const page = await gateway.list({
|
|
52
|
+
collection: collection.name,
|
|
53
|
+
limit: SCAN_PAGE,
|
|
54
|
+
...(cursor === undefined ? {} : { after: cursor }),
|
|
55
|
+
}, context);
|
|
56
|
+
for (const entry of page.items)
|
|
57
|
+
resources.push({ collection, entry });
|
|
58
|
+
read += page.items.length;
|
|
59
|
+
const next = page.nextCursor;
|
|
60
|
+
if (next === null || read >= MAX_SITEMAP_ENTRIES_PER_COLLECTION)
|
|
61
|
+
break;
|
|
62
|
+
cursor = next;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (isCogentaError(error) && error.code === 'FORBIDDEN')
|
|
67
|
+
continue;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return resources;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The `hreflang` alternates for one entry, or an empty list.
|
|
75
|
+
*
|
|
76
|
+
* Skipped outright on a single-locale site: `alternatesFor` would return
|
|
77
|
+
* nothing anyway, and the family lookup is a filtered scan of the collection —
|
|
78
|
+
* work every page render on a monolingual site would pay for a guaranteed
|
|
79
|
+
* empty answer.
|
|
80
|
+
*
|
|
81
|
+
* The family is read through the same permission-checked gateway as the page
|
|
82
|
+
* itself, with the same actor, so a translation the caller may not see never
|
|
83
|
+
* becomes an alternate.
|
|
84
|
+
*/
|
|
85
|
+
export async function alternatesForEntry(site, collection, entry, gateway, context, locales) {
|
|
86
|
+
if (locales.length < 2)
|
|
87
|
+
return [];
|
|
88
|
+
const sourceId = entry.translationOf ?? entry.id;
|
|
89
|
+
const page = await gateway.list({
|
|
90
|
+
collection: collection.name,
|
|
91
|
+
filter: {
|
|
92
|
+
or: [
|
|
93
|
+
{ field: 'id', operator: 'eq', value: sourceId },
|
|
94
|
+
{ field: 'translationOf', operator: 'eq', value: sourceId },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
limit: SCAN_PAGE,
|
|
98
|
+
}, context);
|
|
99
|
+
const resources = page.items.map((found) => ({ collection, entry: found }));
|
|
100
|
+
const family = groupTranslationFamilies(site, resources).find((candidate) => candidate.sourceId === sourceId);
|
|
101
|
+
// A family of one is not a family: a self-referencing `hreflang` is noise,
|
|
102
|
+
// and `buildHreflangMap` drops it for the same reason.
|
|
103
|
+
if (family === undefined || family.members.length < 2)
|
|
104
|
+
return [];
|
|
105
|
+
return alternatesFor(site, family);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The whole `<head>` an entry earns: title, description, canonical,
|
|
109
|
+
* `hreflang`, Open Graph, Twitter Card and a JSON-LD block.
|
|
110
|
+
*
|
|
111
|
+
* Returned as one HTML string because that is what the page template needs;
|
|
112
|
+
* every escaping decision inside it belongs to `@cogenta/seo` (`renderMetaTags`
|
|
113
|
+
* for attributes, `renderJsonLdScript` for the `</script>` hazard), never to
|
|
114
|
+
* this file.
|
|
115
|
+
*/
|
|
116
|
+
export function renderSeoHead(site, resource, options = {}) {
|
|
117
|
+
const tags = buildMetaTags(site, resource, {
|
|
118
|
+
...(options.alternates === undefined ? {} : { alternates: options.alternates }),
|
|
119
|
+
...(options.media === undefined ? {} : { resolvers: { media: options.media } }),
|
|
120
|
+
...(options.noindex === undefined ? {} : { noindex: options.noindex }),
|
|
121
|
+
...(options.title === undefined ? {} : { title: options.title }),
|
|
122
|
+
});
|
|
123
|
+
const parts = [renderMetaTags(tags)];
|
|
124
|
+
const graph = buildJsonLd(site, resource, {
|
|
125
|
+
...(options.media === undefined ? {} : { resolvers: { media: options.media } }),
|
|
126
|
+
});
|
|
127
|
+
if (graph !== null && options.noindex !== true) {
|
|
128
|
+
parts.push(`<script type="application/ld+json">${renderJsonLdScript(graph)}</script>`);
|
|
129
|
+
}
|
|
130
|
+
return parts.join('\n');
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The sitemap files a site needs — one below the protocol limits, an index
|
|
134
|
+
* plus chunks above them.
|
|
135
|
+
*
|
|
136
|
+
* Returned as the whole set rather than one document because the split is not
|
|
137
|
+
* this layer's decision to hide: `serve.ts` routes `/sitemap.xml` *and*
|
|
138
|
+
* `/sitemap-N.xml` from the same array, so a site that grows past 50 000 URLs
|
|
139
|
+
* starts serving an index without anybody changing a route.
|
|
140
|
+
*/
|
|
141
|
+
export function buildSitemapFiles(site, resources) {
|
|
142
|
+
return buildSitemap(site, sitemapUrlsFor(site, resources));
|
|
143
|
+
}
|
|
144
|
+
export function renderRobots(site, options = {}) {
|
|
145
|
+
return renderRobotsTxt({
|
|
146
|
+
site,
|
|
147
|
+
sitemaps: ['/sitemap.xml'],
|
|
148
|
+
// The admin is a signed-in application, not content. Nothing there is
|
|
149
|
+
// reachable without a session anyway, but a crawler has no reason to
|
|
150
|
+
// spend requests discovering that.
|
|
151
|
+
groups: [{ userAgent: '*', allow: ['/'], disallow: ['/admin', '/api/'] }],
|
|
152
|
+
...(options.allowIndexing === undefined ? {} : { allowIndexing: options.allowIndexing }),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=seo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo.js","sourceRoot":"","sources":["../../src/commands/seo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,SAAS,EAAuB,MAAM,cAAc,CAAA;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAE9C,OAAO,EACL,aAAa,EACb,WAAW,EACX,aAAa,EACb,YAAY,EACZ,wBAAwB,EAExB,kBAAkB,EAClB,cAAc,EACd,eAAe,EAKf,cAAc,GACf,MAAM,cAAc,CAAA;AAqBrB;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,IAAkB;IAC3C,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,GAAG;QACjB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,uBAAuB,EAAE,IAAI;KAC9B,CAAA;AACH,CAAC;AAED,qFAAqF;AACrF,MAAM,kCAAkC,GAAG,KAAK,CAAA;AAEhD,6EAA6E;AAC7E,MAAM,SAAS,GAAG,GAAG,CAAA;AAErB;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,WAA4C,EAC5C,OAAuB;IAEvB,MAAM,OAAO,GAAkB,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;IACnD,MAAM,SAAS,GAAkB,EAAE,CAAA;IAEnC,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,IAAI,UAAU,CAAC,OAAO,KAAK,SAAS;YAAE,SAAQ;QAE9C,sEAAsE;QACtE,yEAAyE;QACzE,0EAA0E;QAC1E,iEAAiE;QACjE,qEAAqE;QACrE,wBAAwB;QACxB,IAAI,CAAC;YACH,IAAI,MAA0B,CAAA;YAC9B,IAAI,IAAI,GAAG,CAAC,CAAA;YACZ,SAAS,CAAC;gBACR,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAC7B;oBACE,UAAU,EAAE,UAAU,CAAC,IAAI;oBAC3B,KAAK,EAAE,SAAS;oBAChB,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;iBACnD,EACD,OAAO,CACR,CAAA;gBACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK;oBAAE,SAAS,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;gBACrE,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAA;gBAEzB,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAA;gBAC5B,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,IAAI,kCAAkC;oBAAE,MAAK;gBACtE,MAAM,GAAG,IAAI,CAAA;YACf,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,cAAc,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW;gBAAE,SAAQ;YACjE,MAAM,KAAK,CAAA;QACb,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,IAAa,EACb,UAAgC,EAChC,KAAmB,EACnB,OAAuB,EACvB,OAAsB,EACtB,OAA0B;IAE1B,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,CAAA;IAEjC,MAAM,QAAQ,GAAG,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,EAAE,CAAA;IAChD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAC7B;QACE,UAAU,EAAE,UAAU,CAAC,IAAI;QAC3B,MAAM,EAAE;YACN,EAAE,EAAE;gBACF,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE;gBAChD,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE;aAC5D;SACF;QACD,KAAK,EAAE,SAAS;KACjB,EACD,OAAO,CACR,CAAA;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;IAC3E,MAAM,MAAM,GAAG,wBAAwB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,IAAI,CAC3D,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,KAAK,QAAQ,CAC/C,CAAA;IACD,2EAA2E;IAC3E,uDAAuD;IACvD,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,CAAA;IAChE,OAAO,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;AACpC,CAAC;AAYD;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAa,EACb,QAAqB,EACrB,OAAO,GAAgB,EAAE;IAEzB,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,QAAQ,EAAE;QACzC,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;QAC/E,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAC/E,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;QACtE,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;KACjE,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAA;IAEpC,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE;QACxC,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;KAChF,CAAC,CAAA;IACF,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAC/C,KAAK,CAAC,IAAI,CAAC,sCAAsC,kBAAkB,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IACxF,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAa,EACb,SAAiC;IAEjC,OAAO,YAAY,CAAC,IAAI,EAAE,cAAc,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAA;AAC5D,CAAC;AAOD,MAAM,UAAU,YAAY,CAAC,IAAa,EAAE,OAAO,GAAwB,EAAE;IAC3E,OAAO,eAAe,CAAC;QACrB,IAAI;QACJ,QAAQ,EAAE,CAAC,cAAc,CAAC;QAC1B,sEAAsE;QACtE,qEAAqE;QACrE,mCAAmC;QACnC,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC;QACzE,GAAG,CAAC,OAAO,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC;KACzF,CAAC,CAAA;AACJ,CAAC"}
|
package/dist/commands/serve.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
-
import { type AgentsRouter, type AuditRouter, type AuthRouter, createContentGateway, type MediaRouter, type RestRouter } from '@cogenta/api';
|
|
2
|
+
import { type AgentsRouter, type AssistantRouter, type AuditRouter, type AuthRouter, createContentGateway, type MediaImageProcessor, type MediaRouter, type NoticeRouter, type PermissionLayer, type RestRouter, type SearchRouter, type SitePlanRouter, type TaxonomyRouter, type UsersRouter } from '@cogenta/api';
|
|
3
3
|
import { type AuthStore } from '@cogenta/auth';
|
|
4
4
|
import { type DatabaseHandle, type HealthReport, type Logger, type MediaStore, type StorageDriver } from '@cogenta/core';
|
|
5
|
-
import { type CollectionDefinition, type SchemaDocument } from '@cogenta/schema';
|
|
5
|
+
import { type CollectionDefinition, type RedirectStore, type SchemaDocument, type TaxonomyDefinition } from '@cogenta/schema';
|
|
6
6
|
import type { GraphQLSchema } from 'graphql';
|
|
7
7
|
import type { Output, Writer } from '../output.js';
|
|
8
|
+
import { type SecurityConfig } from './http-security.js';
|
|
9
|
+
import { type SecurityAlertWatch } from './security-alerts.js';
|
|
8
10
|
/**
|
|
9
11
|
* Loads a project's content model.
|
|
10
12
|
*
|
|
@@ -15,6 +17,30 @@ import type { Output, Writer } from '../output.js';
|
|
|
15
17
|
* collections has nothing to serve.
|
|
16
18
|
*/
|
|
17
19
|
export declare function loadCollections(projectRoot: string): Promise<readonly CollectionDefinition[]>;
|
|
20
|
+
/** What a project's schema file declares: collections, and since `schema@2.0` taxonomies. */
|
|
21
|
+
export interface LoadedSchema {
|
|
22
|
+
readonly collections: readonly CollectionDefinition[];
|
|
23
|
+
readonly taxonomies: readonly TaxonomyDefinition[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The same file, read for both halves of the content model.
|
|
27
|
+
*
|
|
28
|
+
* Taxonomies arrive as a **named** export beside the default one
|
|
29
|
+
* (`export const taxonomies = [...]`), so every schema file written before
|
|
30
|
+
* `schema@2.0` keeps loading unchanged and simply declares none.
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadSchemaModule(projectRoot: string): Promise<LoadedSchema>;
|
|
33
|
+
/**
|
|
34
|
+
* The schema file this project actually loads, or `undefined` when it has
|
|
35
|
+
* none.
|
|
36
|
+
*
|
|
37
|
+
* Anything that *writes* the schema back has to target this, not a guessed
|
|
38
|
+
* name: `loadCollections` prefers `cogenta.schema.ts` (the form ADR-0010
|
|
39
|
+
* calls for — TypeScript in git), so a writer that assumed `.mjs` would
|
|
40
|
+
* create tables and then write a file nothing reads, leaving an operator
|
|
41
|
+
* with orphan tables and no collections after the restart it was told to do.
|
|
42
|
+
*/
|
|
43
|
+
export declare function findSchemaFile(projectRoot: string): Promise<string | undefined>;
|
|
18
44
|
interface Site {
|
|
19
45
|
readonly db: DatabaseHandle;
|
|
20
46
|
readonly auth: AuthStore;
|
|
@@ -22,24 +48,69 @@ interface Site {
|
|
|
22
48
|
readonly authRouter: AuthRouter;
|
|
23
49
|
readonly mediaRouter: MediaRouter;
|
|
24
50
|
readonly auditRouter: AuditRouter;
|
|
51
|
+
/** `GET /api/search` — the full-text index, reachable for the first time (L10 task 3). */
|
|
52
|
+
readonly searchRouter: SearchRouter;
|
|
53
|
+
/** `/api/taxonomies/*` — terms, mounted apart from content because a taxonomy is not a collection (`schema@2.0`, ADR-0022). */
|
|
54
|
+
readonly taxonomyRouter: TaxonomyRouter;
|
|
55
|
+
/** ADR-0021's half that replaces the MFA sign-in gate: recommendations the admin shows, never a block. */
|
|
56
|
+
readonly noticeRouter: NoticeRouter;
|
|
57
|
+
/** Refused sign-ins, watched for a run worth alerting on (L14 task 4). `null` when nothing is configured to receive one. */
|
|
58
|
+
readonly securityAlerts: SecurityAlertWatch | null;
|
|
59
|
+
/** Account management from the admin instead of `cogenta users create` on a terminal (L11 task 3). */
|
|
60
|
+
readonly usersRouter: UsersRouter;
|
|
25
61
|
/** Only set when a caller passes `agents` into `assembleSite` — no site constructs one today (R2: agents are optional, not a hard dependency of the CMS). */
|
|
26
62
|
readonly agentsRouter?: AgentsRouter;
|
|
63
|
+
/**
|
|
64
|
+
* `/api/site-plans` — L19 task 7's document-driven planning on a live site.
|
|
65
|
+
*
|
|
66
|
+
* Always mounted, even with no LLM provider: the drafts an installer left
|
|
67
|
+
* behind must still be readable, and the router itself answers
|
|
68
|
+
* `SITE_PLAN_NO_PROVIDER` for the routes that would need a model.
|
|
69
|
+
*/
|
|
70
|
+
readonly sitePlanRouter?: SitePlanRouter;
|
|
71
|
+
/**
|
|
72
|
+
* `/api/assistant` — L18. Always mounted, even on a site with no AI provider:
|
|
73
|
+
* it is the route that *answers* `{available: false}`, which is what lets the
|
|
74
|
+
* admin panel disappear instead of erroring.
|
|
75
|
+
*/
|
|
76
|
+
readonly assistantRouter: AssistantRouter;
|
|
27
77
|
/** Not routed through `mediaRouter`: serving a binary body is outside the JSON-only `RestResponse` shape, so the file route is handled directly (same treatment `/api/schema` already gets). */
|
|
28
78
|
readonly mediaStore: MediaStore;
|
|
29
79
|
readonly storage: StorageDriver;
|
|
80
|
+
/** `null` when no image driver loaded — `/_image` then serves originals only. */
|
|
81
|
+
readonly images: MediaImageProcessor | null;
|
|
30
82
|
readonly graphqlSchema: GraphQLSchema;
|
|
31
83
|
readonly gateway: ReturnType<typeof createContentGateway>;
|
|
84
|
+
/**
|
|
85
|
+
* The same layer the gateway and the REST service already ask. Held here so
|
|
86
|
+
* the routes this file serves itself — the page builder's draft render — can
|
|
87
|
+
* ask the one authority too, rather than re-deciding who may edit (R4).
|
|
88
|
+
*/
|
|
89
|
+
readonly permissions: PermissionLayer;
|
|
32
90
|
/** `.cogenta/schema.json`'s in-memory twin — the admin's only view of the collections (never the schema modules themselves, which are Node code). */
|
|
33
91
|
readonly schemaDocument: SchemaDocument;
|
|
92
|
+
/**
|
|
93
|
+
* The redirect table, applied to *every* GET before routing (L10 task 2).
|
|
94
|
+
*
|
|
95
|
+
* It was already reachable through `/api/content/-/by-path`, which only the
|
|
96
|
+
* API's own clients call — a browser asking for a renamed URL never went
|
|
97
|
+
* near it and got a 404 instead of the 301 the rename created.
|
|
98
|
+
*/
|
|
99
|
+
readonly redirects: RedirectStore;
|
|
34
100
|
readonly collections: readonly CollectionDefinition[];
|
|
101
|
+
readonly taxonomies: readonly TaxonomyDefinition[];
|
|
35
102
|
readonly site: {
|
|
36
103
|
readonly name: string;
|
|
37
104
|
readonly url: string;
|
|
38
105
|
readonly locales: readonly string[];
|
|
39
106
|
readonly defaultLocale: string;
|
|
107
|
+
/** Which page answers an unmatched URL (L14 task 2). `/404` by default. */
|
|
108
|
+
readonly notFoundPath: string;
|
|
40
109
|
};
|
|
41
|
-
/** `null` when
|
|
42
|
-
readonly
|
|
110
|
+
/** The skin's custom properties plus the theme's own stylesheet, minified into one. `null` when neither could be loaded — the theme-render fallback serves unstyled HTML rather than refusing. */
|
|
111
|
+
readonly styles: string | null;
|
|
112
|
+
/** CORS, security headers and cache-control, applied to every response (L10 task 6). */
|
|
113
|
+
readonly security: SecurityConfig;
|
|
43
114
|
/** Live, not cached: a driver that just went down must show as down the next time this is called, not until the process restarts. */
|
|
44
115
|
readonly health: () => Promise<{
|
|
45
116
|
readonly database: HealthReport;
|
|
@@ -79,6 +150,18 @@ export interface ServeOptions {
|
|
|
79
150
|
* decision for whoever deploys a read-only instance, not made here.
|
|
80
151
|
*/
|
|
81
152
|
readonly readOnly?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* `cogenta dev` sets this; `cogenta serve` does not.
|
|
155
|
+
*
|
|
156
|
+
* It gates exactly one thing today: whether an approved site plan may be
|
|
157
|
+
* **applied** (L19 task 7). ADR-0010 is explicit — "l'éditeur visuel de
|
|
158
|
+
* schéma écrit ces fichiers, mais uniquement en mode développement. En
|
|
159
|
+
* production le schéma est en lecture seule" — and applying a plan writes
|
|
160
|
+
* `cogenta.schema.*` and creates tables, which is exactly that editor by
|
|
161
|
+
* another name. Proposing and reviewing a plan stay available everywhere;
|
|
162
|
+
* only the write is held to the decision.
|
|
163
|
+
*/
|
|
164
|
+
readonly development?: boolean;
|
|
82
165
|
}
|
|
83
166
|
/**
|
|
84
167
|
* Runs until `options.signal` aborts. Returns 0 on a clean shutdown, 1 if
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AAInF,OAAO,EAEL,KAAK,YAAY,EAEjB,KAAK,WAAW,EAChB,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"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"}
|