@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.
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-BXVsXHD2.js +0 -23
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Headers every response carries, whatever it is.
3
+ *
4
+ * Deliberately short. `X-Content-Type-Options` stops a browser guessing that
5
+ * an uploaded file is a script; `X-Frame-Options` and `Referrer-Policy` are
6
+ * the two others with no plausible downside and a real failure mode without
7
+ * them. Anything beyond that (`Permissions-Policy`, COEP/COOP) breaks real
8
+ * pages depending on what a site embeds, so it belongs in the site's own
9
+ * `csp` string rather than in a default nobody chose.
10
+ */
11
+ const ALWAYS = Object.freeze({
12
+ 'x-content-type-options': 'nosniff',
13
+ 'x-frame-options': 'SAMEORIGIN',
14
+ 'referrer-policy': 'strict-origin-when-cross-origin',
15
+ });
16
+ /** Which cache-control a path class gets. */
17
+ export function cacheControlFor(pathname, security) {
18
+ // Never store an API response: they are per-actor by construction — the same
19
+ // URL answers differently for an editor and for a stranger — and a shared
20
+ // cache that keeps one is a permission leak, not a performance win.
21
+ if (pathname.startsWith('/api/'))
22
+ return 'no-store';
23
+ // The admin shell is a signed-in application. Its hashed assets could be
24
+ // cached forever, but `index.html` must not be, and telling them apart here
25
+ // would duplicate Vite's naming convention in a second place.
26
+ if (pathname === '/admin' || pathname.startsWith('/admin/'))
27
+ return 'no-store';
28
+ // Images and sitemaps set their own, longer, values at their route.
29
+ if (pathname === '/_image' || pathname === '/robots.txt')
30
+ return null;
31
+ if (pathname === '/search')
32
+ return null;
33
+ // A public page: cacheable, briefly. `must-revalidate` rather than a long
34
+ // max-age because publishing is supposed to be visible immediately, and a
35
+ // CMS whose edits appear ten minutes later is a CMS people stop trusting.
36
+ return security.pageMaxAge === 0
37
+ ? 'no-store'
38
+ : `public, max-age=0, s-maxage=${security.pageMaxAge}, must-revalidate`;
39
+ }
40
+ /**
41
+ * The CORS headers this request earns, or an empty object.
42
+ *
43
+ * The origin is echoed rather than reflected blindly: only an exact match in
44
+ * the configured list, or `*` when the list holds it. `Vary: Origin` goes out
45
+ * with any match, because a shared cache that stores one origin's response
46
+ * and serves it to another is the whole class of CORS cache-poisoning bugs.
47
+ */
48
+ export function corsHeadersFor(origin, security) {
49
+ const { cors } = security;
50
+ if (cors.origins.length === 0 || origin === undefined)
51
+ return {};
52
+ const wildcard = cors.origins.includes('*');
53
+ if (!wildcard && !cors.origins.includes(origin))
54
+ return {};
55
+ return {
56
+ 'access-control-allow-origin': wildcard && !cors.credentials ? '*' : origin,
57
+ vary: 'Origin',
58
+ ...(cors.credentials ? { 'access-control-allow-credentials': 'true' } : {}),
59
+ };
60
+ }
61
+ /** The extra headers a preflight needs on top of the ones above. */
62
+ export function preflightHeadersFor(security) {
63
+ return {
64
+ 'access-control-allow-methods': security.cors.methods.join(', '),
65
+ 'access-control-allow-headers': security.cors.headers.join(', '),
66
+ 'access-control-max-age': String(security.cors.maxAge),
67
+ };
68
+ }
69
+ function baseHeadersFor(req, pathname, security) {
70
+ const headers = { ...ALWAYS };
71
+ if (security.csp !== undefined && security.csp !== false) {
72
+ headers['content-security-policy'] = security.csp;
73
+ }
74
+ // Only over TLS. Sent on a plain HTTP response the header is ignored by
75
+ // browsers anyway, and sending it locally is how somebody pins
76
+ // `localhost` to HTTPS and cannot develop for the next year.
77
+ if (security.hstsMaxAge > 0 && isSecure(req)) {
78
+ headers['strict-transport-security'] = `max-age=${security.hstsMaxAge}${security.hstsIncludeSubDomains ? '; includeSubDomains' : ''}`;
79
+ }
80
+ const origin = headerOf(req, 'origin');
81
+ Object.assign(headers, corsHeadersFor(origin, security));
82
+ // A page render is per-actor: `renderRequestedPage` passes the requesting
83
+ // actor's context down, so an editor's `GET /blog/embargoed` returns the
84
+ // draft. `public, s-maxage=…` is precisely the pair RFC 9111 §3.5 says
85
+ // re-authorises a *shared* cache to store a response to a request carrying
86
+ // `Authorization` — a CDN in front of the site would then serve that draft
87
+ // to everyone for a minute. Anything sent with credentials is private and
88
+ // unstorable, whatever class its path belongs to. Found by the security
89
+ // review of L10 task 6.
90
+ const cacheControl = hasCredentials(req)
91
+ ? 'private, no-store'
92
+ : cacheControlFor(pathname, security);
93
+ if (cacheControl !== null)
94
+ headers['cache-control'] = cacheControl;
95
+ return headers;
96
+ }
97
+ /** True when the request carries something that makes its answer actor-specific. */
98
+ function hasCredentials(req) {
99
+ return headerOf(req, 'authorization') !== undefined || headerOf(req, 'cookie') !== undefined;
100
+ }
101
+ function headerOf(req, name) {
102
+ const value = req.headers[name];
103
+ return Array.isArray(value) ? value[0] : value;
104
+ }
105
+ /** True behind a TLS socket, or behind a proxy that says it terminated one. */
106
+ function isSecure(req) {
107
+ if ('encrypted' in req.socket && req.socket.encrypted === true)
108
+ return true;
109
+ return headerOf(req, 'x-forwarded-proto')?.split(',')[0]?.trim() === 'https';
110
+ }
111
+ /**
112
+ * Applies the headers to every response of this request, and answers a CORS
113
+ * preflight outright.
114
+ *
115
+ * Returns `true` when the request is finished — the caller must then do
116
+ * nothing else. Patching `writeHead` rather than calling `setHeader` up front
117
+ * is what makes this survive the routes that pass a header object of their
118
+ * own: `res.writeHead(200, { 'content-type': … })` replaces every header
119
+ * previously set, so setting them early would silently lose them on most of
120
+ * the routes in this file.
121
+ */
122
+ export function applySecurity(req, res, pathname, security) {
123
+ const base = baseHeadersFor(req, pathname, security);
124
+ const original = res.writeHead.bind(res);
125
+ // Both of Node's overloads are accepted, so no existing call site changes.
126
+ // An array-of-raw-headers form exists too and is not used anywhere here; it
127
+ // is passed through untouched rather than half-merged.
128
+ res.writeHead = ((status, reasonOrHeaders, maybeHeaders) => {
129
+ const supplied = typeof reasonOrHeaders === 'string' ? maybeHeaders : (reasonOrHeaders ?? maybeHeaders);
130
+ if (Array.isArray(supplied)) {
131
+ return typeof reasonOrHeaders === 'string'
132
+ ? original(status, reasonOrHeaders, supplied)
133
+ : original(status, supplied);
134
+ }
135
+ const merged = { ...base, ...(supplied ?? {}) };
136
+ return typeof reasonOrHeaders === 'string'
137
+ ? original(status, reasonOrHeaders, merged)
138
+ : original(status, merged);
139
+ });
140
+ if (req.method === 'OPTIONS') {
141
+ const origin = headerOf(req, 'origin');
142
+ const allowed = corsHeadersFor(origin, security);
143
+ // A preflight for an origin that is not allowed gets a plain 204 with no
144
+ // CORS headers: the browser then refuses the real request on its own. A
145
+ // 403 would be no more secure and would look like a server fault in the
146
+ // console instead of the policy decision it is.
147
+ res.writeHead(204, Object.keys(allowed).length === 0 ? {} : preflightHeadersFor(security));
148
+ res.end();
149
+ return true;
150
+ }
151
+ return false;
152
+ }
153
+ //# sourceMappingURL=http-security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-security.js","sourceRoot":"","sources":["../../src/commands/http-security.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { type Logger } from '@cogenta/core';
2
+ import type { Output, Writer } from '../output.js';
3
+ export type LinksSubcommand = 'check';
4
+ export interface LinksOptions {
5
+ readonly subcommand: string | undefined;
6
+ readonly cwd?: string;
7
+ readonly env?: Record<string, string | undefined>;
8
+ readonly logger?: Logger;
9
+ readonly out: Output;
10
+ readonly stderr: Writer;
11
+ /** Also follow `http(s)` links that leave the site. Off unless asked. */
12
+ readonly external?: boolean;
13
+ }
14
+ /**
15
+ * `cogenta links check` (L14 task 3).
16
+ *
17
+ * Exit codes are the ones a cron entry can act on: 0 nothing broken, 1 broken
18
+ * links were found or the site could not be read, 2 the command line was
19
+ * wrong. A non-zero exit for "found broken links" is deliberate — that is what
20
+ * makes this usable as a CI or scheduled check rather than something whose
21
+ * output somebody has to read.
22
+ */
23
+ export declare function runLinks(options: LinksOptions): Promise<number>;
24
+ //# sourceMappingURL=links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/commands/links.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,MAAM,EAEZ,MAAM,eAAe,CAAA;AAStB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAGlD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAA;AAErC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,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,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AA+BD;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CA+ErE"}
@@ -0,0 +1,109 @@
1
+ import { dirname, resolve as resolvePath } from 'node:path';
2
+ import process from 'node:process';
3
+ import { createDatabaseRegistry, createLogger, isCogentaError, loadConfig, } from '@cogenta/core';
4
+ import { checkLinks, createContentStore, createSchemaTables, } from '@cogenta/schema';
5
+ import { loadCollections } from './serve.js';
6
+ const USAGE = `Usage
7
+ cogenta links check [--external]
8
+
9
+ Walks every published entry, follows every link it holds, and reports the ones
10
+ that lead nowhere: a reference to a deleted or still-unpublished entry, a
11
+ site-relative path that matches no route, and — with --external — an outside
12
+ URL that answers with an error or cannot be reached at all.
13
+
14
+ Nothing about this runs on its own. This project guarantees no durable
15
+ background worker (rule R1), so "periodically" means a cron entry calling this
16
+ command, not a scheduler pretending to exist inside the site.
17
+ `;
18
+ const REASON_TEXT = {
19
+ unknown_collection: 'points at a collection this site does not have',
20
+ target_missing: 'points at content that does not exist',
21
+ target_unpublished: 'points at content that is not published',
22
+ unroutable_path: 'is a path no route can serve',
23
+ http_error: 'answered with an error status',
24
+ unreachable: 'could not be reached',
25
+ };
26
+ function describe(broken) {
27
+ const target = broken.link.kind === 'url' ? broken.link.href : `${broken.link.collection}/${broken.link.id}`;
28
+ const status = broken.status === undefined ? '' : ` (${broken.status})`;
29
+ return `${broken.collection}/${broken.entryId} [${broken.locale}] ${broken.at}\n → ${target}${status} — ${REASON_TEXT[broken.reason]}`;
30
+ }
31
+ /**
32
+ * `cogenta links check` (L14 task 3).
33
+ *
34
+ * Exit codes are the ones a cron entry can act on: 0 nothing broken, 1 broken
35
+ * links were found or the site could not be read, 2 the command line was
36
+ * wrong. A non-zero exit for "found broken links" is deliberate — that is what
37
+ * makes this usable as a CI or scheduled check rather than something whose
38
+ * output somebody has to read.
39
+ */
40
+ export async function runLinks(options) {
41
+ const { out, stderr } = options;
42
+ const env = options.env ?? process.env;
43
+ const logger = options.logger ?? createLogger({ level: 'silent' });
44
+ if (options.subcommand === undefined) {
45
+ stderr(`cogenta links needs a subcommand.\n\n${USAGE}`);
46
+ return 2;
47
+ }
48
+ if (options.subcommand !== 'check') {
49
+ stderr(`Unknown subcommand "${options.subcommand}". Only "check" exists today.\n\n${USAGE}`);
50
+ return 2;
51
+ }
52
+ const loaded = await loadConfig({
53
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
54
+ env,
55
+ });
56
+ const projectRoot = loaded.path === null ? resolvePath(options.cwd ?? process.cwd()) : dirname(loaded.path);
57
+ let selection = null;
58
+ try {
59
+ const collections = await loadCollections(projectRoot);
60
+ selection = await createDatabaseRegistry({ logger }).select(loaded.config.database);
61
+ const db = selection.instance;
62
+ await createSchemaTables(db, collections);
63
+ const stores = new Map();
64
+ const storeFor = (collection) => {
65
+ const existing = stores.get(collection.name);
66
+ if (existing !== undefined)
67
+ return existing;
68
+ const created = createContentStore({ db, collection });
69
+ stores.set(collection.name, created);
70
+ return created;
71
+ };
72
+ const report = await checkLinks({
73
+ collections,
74
+ storeFor,
75
+ locales: loaded.config.site.locales,
76
+ defaultLocale: loaded.config.site.defaultLocale,
77
+ ...(options.external === true ? { checkExternal: true } : {}),
78
+ });
79
+ if (report.broken.length === 0) {
80
+ out.ok(`Checked ${report.checkedLinks} link(s) across ${report.checkedEntries} published entries. Nothing broken.`);
81
+ if (report.skippedExternal > 0) {
82
+ out.detail(`${report.skippedExternal} external link(s) were not followed. Pass --external to check them.`);
83
+ }
84
+ return 0;
85
+ }
86
+ out.warn(`${report.broken.length} broken link(s) in ${report.checkedEntries} published entries:`);
87
+ for (const broken of report.broken)
88
+ out.detail(describe(broken));
89
+ if (report.skippedExternal > 0) {
90
+ out.detail(`${report.skippedExternal} external link(s) were not followed. Pass --external to check them.`);
91
+ }
92
+ return 1;
93
+ }
94
+ catch (error) {
95
+ if (isCogentaError(error)) {
96
+ stderr(`${error.code}: ${error.message}\n`);
97
+ if (error.hint !== undefined)
98
+ stderr(`${error.hint}\n`);
99
+ }
100
+ else {
101
+ stderr(`${error instanceof Error ? error.stack : String(error)}\n`);
102
+ }
103
+ return 1;
104
+ }
105
+ finally {
106
+ await selection?.instance.close();
107
+ }
108
+ }
109
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/commands/links.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import type { ImageSize, MediaImageProcessor } from '@cogenta/api';
2
+ import type { Logger } from '@cogenta/core';
3
+ import { contentTypeOf, type ImageFormat, type ImageTransformer } from '@cogenta/render';
4
+ /**
5
+ * `@cogenta/render`'s image pipeline, wired to the media upload (L10 task 5).
6
+ *
7
+ * The pipeline, `srcset.ts` and the two driver tiers (sharp, WASM libvips)
8
+ * have existed since L3 and were called by nothing. This module is where they
9
+ * meet a real upload.
10
+ *
11
+ * ## Why variants are produced at upload rather than on demand
12
+ *
13
+ * `createImagePipeline` is deliberately lazy — it renders a variant the first
14
+ * time a browser asks for one — and that is the right design for a build step
15
+ * that would otherwise materialise N formats × M widths × K images. It is the
16
+ * wrong design here, and the lot says so explicitly ("au moment de l'upload,
17
+ * pas à la volée"), for a reason worth writing down: `cogenta serve` has no
18
+ * durable variant cache. `createMemoryVariantStore` dies with the process, so
19
+ * a lazy pipeline behind it re-decodes every image after every restart, and
20
+ * on the shared hosting Cogenta targets (R10's own list) that is the slowest
21
+ * possible answer on the coldest possible machine.
22
+ *
23
+ * Producing a bounded, deterministic set once — the `SRCSET_WIDTHS` ladder,
24
+ * capped at the intrinsic width, in WebP — makes the set nameable, which is
25
+ * what lets `variantNames` clean them up on delete without a `list` call the
26
+ * `StorageDriver` interface does not have.
27
+ *
28
+ * ## Why WebP and not AVIF
29
+ *
30
+ * WebP is universally supported in 2026 and encodes in a fraction of AVIF's
31
+ * time. AVIF's extra compression is real, but paying it per upload on a
32
+ * WASM tier — the tier that always exists (R10) — would make an upload of a
33
+ * handful of images take minutes. A second format can be added the day the
34
+ * pipeline is asked for one, and the ladder is the only thing that has to
35
+ * change.
36
+ */
37
+ declare const VARIANT_FORMAT: ImageFormat;
38
+ /** The name of one rendition under an asset's variant prefix. */
39
+ export declare function variantName(width: number, format?: ImageFormat): string;
40
+ /**
41
+ * The widths actually produced for an image.
42
+ *
43
+ * `candidateWidths` caps the ladder at the intrinsic width, so a 500px logo
44
+ * yields 320 and 500 rather than five upscaled copies of itself — upscaling
45
+ * costs bytes and gains nothing.
46
+ */
47
+ export declare function variantWidthsFor(intrinsic: ImageSize): readonly number[];
48
+ export interface MediaImageProcessorOptions {
49
+ readonly transformer: ImageTransformer;
50
+ readonly quality?: number;
51
+ }
52
+ export declare function createMediaImageProcessor(options: MediaImageProcessorOptions): MediaImageProcessor;
53
+ /**
54
+ * The processor for the driver this host can actually run, or `null`.
55
+ *
56
+ * `null` rather than a throw: an install where neither tier loads must still
57
+ * upload and serve images at their original size. That is the same "absent,
58
+ * not broken" degradation R2 applies to the LLM and R1 to every other driver
59
+ * — a CMS that refuses an upload because a WebAssembly module would not boot
60
+ * is worse than one that serves a full-size JPEG.
61
+ */
62
+ export declare function selectMediaImageProcessor(logger: Logger): Promise<{
63
+ processor: MediaImageProcessor;
64
+ driver: string;
65
+ } | null>;
66
+ export { contentTypeOf, VARIANT_FORMAT };
67
+ //# sourceMappingURL=media-images.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-images.d.ts","sourceRoot":"","sources":["../../src/commands/media-images.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAwB,MAAM,cAAc,CAAA;AACxF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAEL,aAAa,EAGb,KAAK,WAAW,EAChB,KAAK,gBAAgB,EAItB,MAAM,iBAAiB,CAAA;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,QAAA,MAAM,cAAc,EAAE,WAAoB,CAAA;AAE1C,iEAAiE;AACjE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,WAA4B,GAAG,MAAM,CAEvF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,MAAM,EAAE,CAExE;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAA;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,0BAA0B,GAClC,mBAAmB,CAsCrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,SAAS,EAAE,mBAAmB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAapE;AAED,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,CAAA"}
@@ -0,0 +1,107 @@
1
+ import { candidateWidths, contentTypeOf, createImageRegistry, DEFAULT_QUALITY, planTransform, SRCSET_WIDTHS, suffixOf, } from '@cogenta/render';
2
+ /**
3
+ * `@cogenta/render`'s image pipeline, wired to the media upload (L10 task 5).
4
+ *
5
+ * The pipeline, `srcset.ts` and the two driver tiers (sharp, WASM libvips)
6
+ * have existed since L3 and were called by nothing. This module is where they
7
+ * meet a real upload.
8
+ *
9
+ * ## Why variants are produced at upload rather than on demand
10
+ *
11
+ * `createImagePipeline` is deliberately lazy — it renders a variant the first
12
+ * time a browser asks for one — and that is the right design for a build step
13
+ * that would otherwise materialise N formats × M widths × K images. It is the
14
+ * wrong design here, and the lot says so explicitly ("au moment de l'upload,
15
+ * pas à la volée"), for a reason worth writing down: `cogenta serve` has no
16
+ * durable variant cache. `createMemoryVariantStore` dies with the process, so
17
+ * a lazy pipeline behind it re-decodes every image after every restart, and
18
+ * on the shared hosting Cogenta targets (R10's own list) that is the slowest
19
+ * possible answer on the coldest possible machine.
20
+ *
21
+ * Producing a bounded, deterministic set once — the `SRCSET_WIDTHS` ladder,
22
+ * capped at the intrinsic width, in WebP — makes the set nameable, which is
23
+ * what lets `variantNames` clean them up on delete without a `list` call the
24
+ * `StorageDriver` interface does not have.
25
+ *
26
+ * ## Why WebP and not AVIF
27
+ *
28
+ * WebP is universally supported in 2026 and encodes in a fraction of AVIF's
29
+ * time. AVIF's extra compression is real, but paying it per upload on a
30
+ * WASM tier — the tier that always exists (R10) — would make an upload of a
31
+ * handful of images take minutes. A second format can be added the day the
32
+ * pipeline is asked for one, and the ladder is the only thing that has to
33
+ * change.
34
+ */
35
+ const VARIANT_FORMAT = 'webp';
36
+ /** The name of one rendition under an asset's variant prefix. */
37
+ export function variantName(width, format = VARIANT_FORMAT) {
38
+ return `${width}.${suffixOf(format)}`;
39
+ }
40
+ /**
41
+ * The widths actually produced for an image.
42
+ *
43
+ * `candidateWidths` caps the ladder at the intrinsic width, so a 500px logo
44
+ * yields 320 and 500 rather than five upscaled copies of itself — upscaling
45
+ * costs bytes and gains nothing.
46
+ */
47
+ export function variantWidthsFor(intrinsic) {
48
+ return candidateWidths(intrinsic.width, intrinsic.width, SRCSET_WIDTHS);
49
+ }
50
+ export function createMediaImageProcessor(options) {
51
+ const { transformer } = options;
52
+ const quality = options.quality ?? DEFAULT_QUALITY;
53
+ return {
54
+ probe: async (bytes) => {
55
+ try {
56
+ const metadata = await transformer.metadata(bytes);
57
+ return { width: metadata.width, height: metadata.height };
58
+ }
59
+ catch {
60
+ // Not an image this tier can decode. The upload is still legitimate —
61
+ // `verifyRealType` already sniffed the container — so the honest
62
+ // answer is "no dimensions", not a refusal.
63
+ return null;
64
+ }
65
+ },
66
+ variants: async (bytes, intrinsic) => {
67
+ const metadata = await transformer.metadata(bytes);
68
+ const produced = [];
69
+ for (const width of variantWidthsFor(intrinsic)) {
70
+ const rendered = await transformer.transform(bytes, planTransform(metadata, { width, format: VARIANT_FORMAT, quality }));
71
+ produced.push({
72
+ name: variantName(width, VARIANT_FORMAT),
73
+ bytes: rendered.bytes,
74
+ contentType: rendered.contentType,
75
+ });
76
+ }
77
+ return produced;
78
+ },
79
+ variantNames: (intrinsic) => variantWidthsFor(intrinsic).map((width) => variantName(width)),
80
+ };
81
+ }
82
+ /**
83
+ * The processor for the driver this host can actually run, or `null`.
84
+ *
85
+ * `null` rather than a throw: an install where neither tier loads must still
86
+ * upload and serve images at their original size. That is the same "absent,
87
+ * not broken" degradation R2 applies to the LLM and R1 to every other driver
88
+ * — a CMS that refuses an upload because a WebAssembly module would not boot
89
+ * is worse than one that serves a full-size JPEG.
90
+ */
91
+ export async function selectMediaImageProcessor(logger) {
92
+ try {
93
+ const selection = await createImageRegistry({ logger }).select({});
94
+ return {
95
+ processor: createMediaImageProcessor({ transformer: selection.instance }),
96
+ driver: selection.driver,
97
+ };
98
+ }
99
+ catch (error) {
100
+ logger.warn('no image driver available; uploads will carry no variants', {
101
+ error: String(error),
102
+ });
103
+ return null;
104
+ }
105
+ }
106
+ export { contentTypeOf, VARIANT_FORMAT };
107
+ //# sourceMappingURL=media-images.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-images.js","sourceRoot":"","sources":["../../src/commands/media-images.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { AccessContext, ContentGateway, SearchRouter } from '@cogenta/api';
2
+ import { type CollectionDefinition } from '@cogenta/schema';
3
+ /**
4
+ * `GET /search?q=…` — the public half of L10 task 3.
5
+ *
6
+ * The lot asks for "un bloc `search` optionnel côté thème public (formulaire
7
+ * + page de résultats)". The form and the results page are here; the **block**
8
+ * deliberately is not.
9
+ *
10
+ * Contract B is frozen (2026-08-13) and AGENTS.md forbids adding a block to
11
+ * the vocabulary without an RFC. A `search` block would be a contract B
12
+ * addition, so it would need a major version bump and a migration note — a
13
+ * decision that does not belong inside a lot whose whole premise is "no new
14
+ * capability, only wiring". A real route serving a real results page gives a
15
+ * visitor the same thing without touching the contract at all; when an RFC
16
+ * for a `search` block does happen, it can render through this same query.
17
+ *
18
+ * The page is `noindex`: a search results page is exactly the kind of thin,
19
+ * infinitely-many-URLs page a crawler must not index, and `buildMetaTags`
20
+ * takes `noindex` as an option precisely for it.
21
+ */
22
+ export interface SearchPageOptions {
23
+ readonly router: SearchRouter;
24
+ readonly gateway: ContentGateway;
25
+ readonly collections: readonly CollectionDefinition[];
26
+ readonly site: {
27
+ readonly name: string;
28
+ readonly url: string;
29
+ readonly locales: readonly string[];
30
+ readonly defaultLocale: string;
31
+ };
32
+ /** The joined skin+theme stylesheet `cogenta serve` serves at `STYLESHEET_PATH` (`theme-render.ts`'s `joinStyles`). `null` when neither could be loaded — served unstyled rather than refused. */
33
+ readonly styles: string | null;
34
+ }
35
+ /**
36
+ * The whole page. Returns HTML; the caller decides the status code (always
37
+ * 200 — an empty result set is an answer, not a failure).
38
+ */
39
+ export declare function renderSearchPage(query: string, options: SearchPageOptions, context: AccessContext): Promise<string>;
40
+ //# sourceMappingURL=search-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-page.d.ts","sourceRoot":"","sources":["../../src/commands/search-page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC/E,OAAO,EAAa,KAAK,oBAAoB,EAAkB,MAAM,iBAAiB,CAAA;AAItF;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,SAAS,oBAAoB,EAAE,CAAA;IACrD,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;KAC/B,CAAA;IACD,kMAAkM;IAClM,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AA+ED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,CAmDjB"}
@@ -0,0 +1,104 @@
1
+ import { buildPath } from '@cogenta/schema';
2
+ import { escapeHtmlAttribute, escapeHtmlText } from '@cogenta/seo';
3
+ import { STYLESHEET_PATH } from './theme-render.js';
4
+ /**
5
+ * Turns hits into links.
6
+ *
7
+ * The hit itself carries no URL — the index stores text, not routes — so each
8
+ * one is read back through the same permission-checked gateway that answered
9
+ * the search. An entry in a collection with no `routing`, or whose route
10
+ * parameters are incomplete, is listed without a link rather than with a URL
11
+ * that 404s.
12
+ */
13
+ async function resolveHits(hits, options, context) {
14
+ const byName = new Map(options.collections.map((collection) => [collection.name, collection]));
15
+ const resolved = [];
16
+ for (const hit of hits) {
17
+ const collection = byName.get(hit.collection);
18
+ if (collection === undefined)
19
+ continue;
20
+ let href = null;
21
+ if (collection.routing !== undefined) {
22
+ const entry = await options.gateway.read(hit.collection, hit.id, context);
23
+ if (entry !== null) {
24
+ const params = Object.fromEntries(Object.entries(entry.values).filter((pair) => typeof pair[1] === 'string'));
25
+ const complete = collection.routing.pattern
26
+ .split('/')
27
+ .filter((segment) => segment.startsWith(':'))
28
+ .every((segment) => params[segment.slice(1)] !== undefined || segment === ':id');
29
+ if (complete) {
30
+ href = buildPath(collection, { ...params, id: entry.id }, collection.routing.locale === true ? entry.locale : undefined);
31
+ }
32
+ }
33
+ }
34
+ resolved.push({ title: hit.title.length > 0 ? hit.title : hit.id, href });
35
+ }
36
+ return resolved;
37
+ }
38
+ /** The form on its own, so an empty query still gets a usable page. */
39
+ function searchForm(query) {
40
+ return `<form class="cg-search__form" action="/search" method="get" role="search">
41
+ <label for="cg-search-q">Search</label>
42
+ <input id="cg-search-q" type="search" name="q" value="${escapeHtmlAttribute(query)}" required>
43
+ <button type="submit">Search</button>
44
+ </form>`;
45
+ }
46
+ function resultList(results) {
47
+ if (results.length === 0)
48
+ return `<p class="cg-search__empty">Nothing matched that search.</p>`;
49
+ return `<ol class="cg-search__results">
50
+ ${results
51
+ .map((result) => result.href === null
52
+ ? `<li>${escapeHtmlText(result.title)}</li>`
53
+ : `<li><a href="${escapeHtmlAttribute(result.href)}">${escapeHtmlText(result.title)}</a></li>`)
54
+ .join('\n')}
55
+ </ol>`;
56
+ }
57
+ /**
58
+ * The whole page. Returns HTML; the caller decides the status code (always
59
+ * 200 — an empty result set is an answer, not a failure).
60
+ */
61
+ export async function renderSearchPage(query, options, context) {
62
+ const trimmed = query.trim();
63
+ let results = [];
64
+ let failure = null;
65
+ if (trimmed.length > 0) {
66
+ const response = await options.router.handle({ method: 'GET', path: '/api/search', query: { q: trimmed } }, context);
67
+ if (response.status === 200) {
68
+ const body = response.body;
69
+ results = await resolveHits(body.data, options, context);
70
+ }
71
+ else {
72
+ // The router's own message, never a rewritten one: it already says what
73
+ // failed and what to do about it, and inventing a second wording here
74
+ // is how the two drift apart.
75
+ const body = response.body;
76
+ failure = body.error?.message ?? 'The search could not be completed.';
77
+ }
78
+ }
79
+ const heading = trimmed.length === 0 ? 'Search' : `Search results for “${escapeHtmlText(trimmed)}”`;
80
+ const main = failure !== null
81
+ ? `<p class="cg-search__error" role="alert">${escapeHtmlText(failure)}</p>`
82
+ : trimmed.length === 0
83
+ ? ''
84
+ : resultList(results);
85
+ return `<!doctype html>
86
+ <html lang="${escapeHtmlAttribute(options.site.defaultLocale)}">
87
+ <head>
88
+ <meta charset="utf-8">
89
+ <meta name="viewport" content="width=device-width, initial-scale=1">
90
+ <title>${heading} — ${escapeHtmlText(options.site.name)}</title>
91
+ <meta name="robots" content="noindex, follow" />
92
+ ${options.styles === null ? '' : `<link rel="stylesheet" href="${STYLESHEET_PATH}">`}
93
+ </head>
94
+ <body>
95
+ <main class="cg-main" id="cg-main">
96
+ <h1 class="cg-page__title">${heading}</h1>
97
+ ${searchForm(trimmed)}
98
+ ${main}
99
+ </main>
100
+ </body>
101
+ </html>
102
+ `;
103
+ }
104
+ //# sourceMappingURL=search-page.js.map