@cogenta/cli 0.2.0 → 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.
Files changed (62) hide show
  1. package/dist/admin-assets/assets/index-Buwdj1V2.js +71 -0
  2. package/dist/admin-assets/assets/index-Csef0SiA.css +1 -0
  3. package/dist/admin-assets/index.html +2 -2
  4. package/dist/commands/assistant.d.ts +65 -0
  5. package/dist/commands/assistant.d.ts.map +1 -0
  6. package/dist/commands/assistant.js +207 -0
  7. package/dist/commands/assistant.js.map +1 -0
  8. package/dist/commands/content-webhooks.d.ts +41 -0
  9. package/dist/commands/content-webhooks.d.ts.map +1 -0
  10. package/dist/commands/content-webhooks.js +61 -0
  11. package/dist/commands/content-webhooks.js.map +1 -0
  12. package/dist/commands/http-security.d.ts +39 -0
  13. package/dist/commands/http-security.d.ts.map +1 -0
  14. package/dist/commands/http-security.js +153 -0
  15. package/dist/commands/http-security.js.map +1 -0
  16. package/dist/commands/links.d.ts +24 -0
  17. package/dist/commands/links.d.ts.map +1 -0
  18. package/dist/commands/links.js +109 -0
  19. package/dist/commands/links.js.map +1 -0
  20. package/dist/commands/media-images.d.ts +67 -0
  21. package/dist/commands/media-images.d.ts.map +1 -0
  22. package/dist/commands/media-images.js +107 -0
  23. package/dist/commands/media-images.js.map +1 -0
  24. package/dist/commands/search-page.d.ts +40 -0
  25. package/dist/commands/search-page.d.ts.map +1 -0
  26. package/dist/commands/search-page.js +104 -0
  27. package/dist/commands/search-page.js.map +1 -0
  28. package/dist/commands/security-alerts.d.ts +24 -0
  29. package/dist/commands/security-alerts.d.ts.map +1 -0
  30. package/dist/commands/security-alerts.js +82 -0
  31. package/dist/commands/security-alerts.js.map +1 -0
  32. package/dist/commands/seo.d.ts +88 -0
  33. package/dist/commands/seo.d.ts.map +1 -0
  34. package/dist/commands/seo.js +155 -0
  35. package/dist/commands/seo.js.map +1 -0
  36. package/dist/commands/serve.d.ts +87 -4
  37. package/dist/commands/serve.d.ts.map +1 -1
  38. package/dist/commands/serve.js +740 -40
  39. package/dist/commands/serve.js.map +1 -1
  40. package/dist/commands/site-plan.d.ts +86 -0
  41. package/dist/commands/site-plan.d.ts.map +1 -0
  42. package/dist/commands/site-plan.js +235 -0
  43. package/dist/commands/site-plan.js.map +1 -0
  44. package/dist/commands/theme-css.d.ts +54 -0
  45. package/dist/commands/theme-css.d.ts.map +1 -0
  46. package/dist/commands/theme-css.js +121 -0
  47. package/dist/commands/theme-css.js.map +1 -0
  48. package/dist/commands/theme-render.d.ts +78 -4
  49. package/dist/commands/theme-render.d.ts.map +1 -1
  50. package/dist/commands/theme-render.js +170 -16
  51. package/dist/commands/theme-render.js.map +1 -1
  52. package/dist/commands/users.d.ts +7 -1
  53. package/dist/commands/users.d.ts.map +1 -1
  54. package/dist/commands/users.js +159 -15
  55. package/dist/commands/users.js.map +1 -1
  56. package/dist/index.d.ts +2 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +31 -0
  59. package/dist/index.js.map +1 -1
  60. package/package.json +12 -10
  61. package/dist/admin-assets/assets/index-21ZcDkDC.css +0 -1
  62. package/dist/admin-assets/assets/index-CKLmd_Fi.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":"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"}
@@ -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 the project has no `theme.tokens.json` — the theme-render fallback serves unstyled HTML rather than refusing. */
42
- readonly skinCss: string | null;
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":"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"}
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,eAAe,EAEpB,KAAK,WAAW,EAChB,KAAK,UAAU,EAMf,oBAAoB,EAepB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,eAAe,EAGpB,KAAK,UAAU,EAEf,KAAK,YAAY,EACjB,KAAK,cAAc,EAEnB,KAAK,cAAc,EACnB,KAAK,WAAW,EAEjB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,KAAK,SAAS,EAAmB,MAAM,eAAe,CAAA;AAC/D,OAAO,EAML,KAAK,cAAc,EACnB,KAAK,YAAY,EAEjB,KAAK,MAAM,EAEX,KAAK,UAAU,EACf,KAAK,aAAa,EACnB,MAAM,eAAe,CAAA;AAEtB,OAAO,EAGL,KAAK,oBAAoB,EAQzB,KAAK,aAAa,EAClB,KAAK,cAAc,EAEnB,KAAK,kBAAkB,EAKxB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAIlD,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAGvE,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AA+BxF;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,SAAS,oBAAoB,EAAE,CAAC,CAE1C;AAED,6FAA6F;AAC7F,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,WAAW,EAAE,SAAS,oBAAoB,EAAE,CAAA;IACrD,QAAQ,CAAC,UAAU,EAAE,SAAS,kBAAkB,EAAE,CAAA;CACnD;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAgDjF;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAWrF;AAuCD,UAAU,IAAI;IACZ,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAA;IAC/B,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAA;IAC/B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,0FAA0F;IAC1F,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAA;IACnC,+HAA+H;IAC/H,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAA;IACvC,0GAA0G;IAC1G,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAA;IACnC,4HAA4H;IAC5H,QAAQ,CAAC,cAAc,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAClD,sGAAsG;IACtG,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,6JAA6J;IAC7J,QAAQ,CAAC,YAAY,CAAC,EAAE,YAAY,CAAA;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAA;IACxC;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAA;IACzC,gMAAgM;IAChM,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAA;IAC/B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAAA;IAC3C,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAA;IACrC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAA;IACzD;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,eAAe,CAAA;IACrC,qJAAqJ;IACrJ,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAA;IACvC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAA;IACjC,QAAQ,CAAC,WAAW,EAAE,SAAS,oBAAoB,EAAE,CAAA;IACrD,QAAQ,CAAC,UAAU,EAAE,SAAS,kBAAkB,EAAE,CAAA;IAClD,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;QACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;QACpB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;QACnC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;QAC9B,2EAA2E;QAC3E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;KAC9B,CAAA;IACD,kMAAkM;IAClM,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAA;IACjC,qIAAqI;IACrI,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC;QAC7B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;QAC/B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;KAC/B,CAAC,CAAA;IACF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AA+rBD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,MAAM,GACb,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CA2e9D;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,wFAAwF;IACxF,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IAC/D,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAC/B;AAQD;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAgJrE"}