@cogenta/core 0.4.0 → 0.7.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/CHANGELOG.md +3532 -0
- package/dist/config/env.d.ts +21 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +72 -0
- package/dist/config/env.js.map +1 -1
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/load-config.d.ts +9 -0
- package/dist/config/load-config.d.ts.map +1 -1
- package/dist/config/load-config.js +39 -15
- package/dist/config/load-config.js.map +1 -1
- package/dist/config/resolve-config.d.ts.map +1 -1
- package/dist/config/resolve-config.js +39 -0
- package/dist/config/resolve-config.js.map +1 -1
- package/dist/config/schema.d.ts +49 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +149 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/config/secret-hygiene.d.ts +55 -0
- package/dist/config/secret-hygiene.d.ts.map +1 -0
- package/dist/config/secret-hygiene.js +94 -0
- package/dist/config/secret-hygiene.js.map +1 -0
- package/dist/config/types.d.ts +166 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +4 -0
- package/dist/config/types.js.map +1 -1
- package/dist/drivers/index.d.ts +1 -1
- package/dist/drivers/index.d.ts.map +1 -1
- package/dist/drivers/index.js.map +1 -1
- package/dist/drivers/registry.d.ts.map +1 -1
- package/dist/drivers/registry.js +15 -8
- package/dist/drivers/registry.js.map +1 -1
- package/dist/drivers/types.d.ts +23 -0
- package/dist/drivers/types.d.ts.map +1 -1
- package/dist/errors/codes.d.ts +1 -1
- package/dist/errors/codes.d.ts.map +1 -1
- package/dist/errors/codes.js +244 -0
- package/dist/errors/codes.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger/error-log.d.ts +48 -0
- package/dist/logger/error-log.d.ts.map +1 -0
- package/dist/logger/error-log.js +57 -0
- package/dist/logger/error-log.js.map +1 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +1 -0
- package/dist/logger/index.js.map +1 -1
- package/dist/media/exif.d.ts +46 -0
- package/dist/media/exif.d.ts.map +1 -0
- package/dist/media/exif.js +291 -0
- package/dist/media/exif.js.map +1 -0
- package/dist/media/folder-path.d.ts +16 -0
- package/dist/media/folder-path.d.ts.map +1 -0
- package/dist/media/folder-path.js +51 -0
- package/dist/media/folder-path.js.map +1 -0
- package/dist/media/folder-store.d.ts +26 -0
- package/dist/media/folder-store.d.ts.map +1 -0
- package/dist/media/folder-store.js +361 -0
- package/dist/media/folder-store.js.map +1 -0
- package/dist/media/index.d.ts +5 -2
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +4 -1
- package/dist/media/index.js.map +1 -1
- package/dist/media/sql-fragments.d.ts +19 -0
- package/dist/media/sql-fragments.d.ts.map +1 -0
- package/dist/media/sql-fragments.js +29 -0
- package/dist/media/sql-fragments.js.map +1 -0
- package/dist/media/store.d.ts +1 -0
- package/dist/media/store.d.ts.map +1 -1
- package/dist/media/store.js +175 -20
- package/dist/media/store.js.map +1 -1
- package/dist/media/types.d.ts +148 -0
- package/dist/media/types.d.ts.map +1 -1
- package/dist/queue/bullmq.d.ts +4 -0
- package/dist/queue/bullmq.d.ts.map +1 -1
- package/dist/queue/bullmq.js +53 -10
- package/dist/queue/bullmq.js.map +1 -1
- package/dist/queue/database.d.ts.map +1 -1
- package/dist/queue/database.js +32 -11
- package/dist/queue/database.js.map +1 -1
- package/dist/queue/types.d.ts +19 -0
- package/dist/queue/types.d.ts.map +1 -1
- package/dist/rate-limit/index.d.ts +21 -0
- package/dist/rate-limit/index.d.ts.map +1 -0
- package/dist/rate-limit/index.js +25 -0
- package/dist/rate-limit/index.js.map +1 -0
- package/dist/rate-limit/memory.d.ts +17 -0
- package/dist/rate-limit/memory.d.ts.map +1 -0
- package/dist/rate-limit/memory.js +61 -0
- package/dist/rate-limit/memory.js.map +1 -0
- package/dist/rate-limit/redis.d.ts +55 -0
- package/dist/rate-limit/redis.d.ts.map +1 -0
- package/dist/rate-limit/redis.js +134 -0
- package/dist/rate-limit/redis.js.map +1 -0
- package/dist/rate-limit/types.d.ts +52 -0
- package/dist/rate-limit/types.d.ts.map +1 -0
- package/dist/rate-limit/types.js +2 -0
- package/dist/rate-limit/types.js.map +1 -0
- package/dist/version.d.ts +39 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +84 -0
- package/dist/version.js.map +1 -0
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,3532 @@
|
|
|
1
|
+
# @cogenta/core
|
|
2
|
+
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`89e7579`](https://github.com/cogenta-cms/cogenta/commit/89e7579129712a5978ff57b884151731f5c340ea) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Fiche feedback, two bugs reported live from the same test session: "je ne sais pas si le
|
|
8
|
+
traitement est en cours ou pas" (no feedback during a long agent run or theme generation),
|
|
9
|
+
and a reference screenshot attached to "personnalise le theme actuel pour qu'il soit comme
|
|
10
|
+
cette capture" that only ever changed accent colors, never the layout it showed.
|
|
11
|
+
|
|
12
|
+
**The reference-image bug, fixed**: `propose-theme.ts`'s `chooseTheme()` step already
|
|
13
|
+
received an attached image, but `generateSkinCandidates()` (the step that actually
|
|
14
|
+
produces the color/font/spacing tokens) never did — `generateSkin` had zero image support
|
|
15
|
+
at all. `@cogenta/agents`'s `GenerateSkinOptions`/`GenerateSkinCandidatesOptions` gain an
|
|
16
|
+
optional `images: readonly ChatImagePart[]`, threaded through to the model call alongside
|
|
17
|
+
an explicit "derive the colour palette from it" instruction when present.
|
|
18
|
+
|
|
19
|
+
**What is still a genuine limit, not fixed here**: a theme's page *structure* (which
|
|
20
|
+
blocks render where) is owned by the installed theme package's own render code — contract
|
|
21
|
+
D only ever describes tokens (color, font, spacing, radius, shadow, motion). No amount of
|
|
22
|
+
image analysis can turn a token-generation call into a layout generator; that would be a
|
|
23
|
+
different, much larger feature.
|
|
24
|
+
|
|
25
|
+
**Live progress**: new `@cogenta/agents` module `progress/` — `ProgressReporter`,
|
|
26
|
+
`ProgressEvent`, `createProgressJobStore<TResult>()` (in-memory, per-process; a progress
|
|
27
|
+
job is short-lived and watched by one open tab, unlike `@cogenta/core`'s durable `queue`).
|
|
28
|
+
`RunAgentLoopInput.onProgress`/`RunAgentOptions.onProgress` report `"Thinking…"`,
|
|
29
|
+
`"Calling tool "X"…"`, retry attempts, and tool outcomes as the agent loop runs — the same
|
|
30
|
+
engine behind both agent chat and every other agent, so instrumenting it once covers both.
|
|
31
|
+
`GenerateSkinCandidatesOptions.onProgress` and `ProposeThemeCandidatesInput.onProgress`
|
|
32
|
+
report each design direction as it starts/finishes. `RetryOptions.onRetry` is a new hook
|
|
33
|
+
`retryModelCall` invokes before backing off.
|
|
34
|
+
|
|
35
|
+
`@cogenta/core` gains two error codes, `AGENT_RUN_JOB_UNKNOWN` and
|
|
36
|
+
`THEME_GENERATE_JOB_UNKNOWN` (mapped to 404).
|
|
37
|
+
|
|
38
|
+
`@cogenta/api` adds three watchable job route pairs, additive alongside the existing
|
|
39
|
+
synchronous ones (nothing is removed or changed for a caller that doesn't care about
|
|
40
|
+
progress): `POST/GET /api/agents/:name/conversation/jobs[/:jobId]`,
|
|
41
|
+
`POST/GET /api/agents/:name/run/jobs[/:jobId]`, `POST/GET /api/theme/generate/jobs[/:jobId]`.
|
|
42
|
+
`AgentsRouterOptions`/`ThemeRouterOptions` gain an optional `progressJobs` store; without
|
|
43
|
+
one, only the job routes are unavailable — the pre-existing synchronous routes are
|
|
44
|
+
untouched.
|
|
45
|
+
|
|
46
|
+
`@cogenta/cli` wires `createProgressJobStore()` into `cogenta serve`'s agents and theme
|
|
47
|
+
routers, and forwards `onProgress` through `agent-runtime.ts`'s `AgentRunnerLike` adapter
|
|
48
|
+
(a real gap caught by an e2e test: the adapter's `run()` silently dropped the parameter
|
|
49
|
+
before this fix).
|
|
50
|
+
|
|
51
|
+
Admin UI (private, no changeset): the floating chat widget and the theme generator
|
|
52
|
+
workshop both poll their job's `GET` endpoint and render a live, growing progress list
|
|
53
|
+
(`data-testid="agent-chat-progress"` / `"theme-generator-progress"`, both `aria-live="polite"`)
|
|
54
|
+
instead of a static "thinking…" placeholder.
|
|
55
|
+
|
|
56
|
+
## 0.6.0
|
|
57
|
+
|
|
58
|
+
### Minor Changes
|
|
59
|
+
|
|
60
|
+
- [`b85ce4e`](https://github.com/cogenta-cms/cogenta/commit/b85ce4edad72ff065cd63c852a9f42aeefc5ab9a) Thanks [@georgesmomo](https://github.com/georgesmomo)! - A tax rule or shipping method could previously only be created or deleted — fixing a typo'd rate, label, or zone meant deleting and recreating it, losing its `createdAt` and (for a shipping method) its `position` among the other methods.
|
|
61
|
+
|
|
62
|
+
- `TaxStore.updateRule()` and `ShippingStore.updateMethod()` (`@cogenta/commerce`) accept a tri-state patch: a field absent from the patch is left exactly as saved, and for the nullable fields (`country`/`region`/`freeOverMinor`/`carrier`) an explicit `null` clears them back to what an absent field already means at creation time.
|
|
63
|
+
- `PATCH /api/commerce/tax/rules/:id` and `PATCH /api/commerce/shipping/methods/:id` carry the same semantics — still gated on `commerce.catalog.write`, unchanged authorization.
|
|
64
|
+
- `@cogenta/core` gains the `COMMERCE_TAX_RULE_UNKNOWN` error code for an edit naming an id that was never a rule (shipping reuses the existing `COMMERCE_SHIPPING_METHOD_UNKNOWN`).
|
|
65
|
+
- Admin: an "Edit" action on each tax rule and shipping method row opens a dialog — pre-filled from the row — that saves through this PATCH path.
|
|
66
|
+
|
|
67
|
+
- [`bde02b5`](https://github.com/cogenta-cms/cogenta/commit/bde02b518f98a8d4cbc58544ea809c658b8dee7b) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Every hardcoded value that controlled an LLM call's behaviour (output token budget, retry/correction attempts, request timeout) is now a real, admin-configurable property of the provider, editable from `/admin/providers` — never a number guessed in code and shared across every model an operator might configure.
|
|
68
|
+
|
|
69
|
+
An audit of the whole `@cogenta/agents` call surface found the same class of bug independently reproduced and patched in more than one place with a different hardcoded constant each time (theme/skin generation, the generic agent runtime, the delegated-subagent budget, the `assist.*` toolset, the LangGraph loop's per-call timeout) — always the same root cause: a reasoning-tier model (confirmed live against DeepSeek) spends thousands of tokens "thinking" before writing a visible answer, and a fixed ceiling sized for a plain instruct model truncates it to an empty response.
|
|
70
|
+
|
|
71
|
+
- `ProviderClient` gains `maxOutputTokens`/`requestTimeoutMs`/`maxCorrectionAttempts` — resolved once per client from the admin's saved provider config (`StoredProviderConfig`, `ProviderConfigInput`), falling back to a built-in default only when unset. `ChatRequest.maxTokens` is now optional; when a caller omits it, the resolved client's own budget applies.
|
|
72
|
+
- `createAnthropicClient`/`createOpenAiClient`/`createGoogleClient` accept these three as config and apply them to every request and to the per-call HTTP timeout (`requestSignalWithTimeout`).
|
|
73
|
+
- Every call site that used to hardcode its own `MAX_TOKENS`/`DEFAULT_MAX_ATTEMPTS` (skin generation, the base-theme choice, brief analysis, content-model/demo-content proposals, the generic agent loop, the delegated-subagent tool, the `assist.*` toolset) now defers to the resolved client instead — removing eight separately-guessed numbers, not just the one already fixed for skin generation.
|
|
74
|
+
- New error code `PROVIDER_TUNING_INVALID` — a saved value outside sane bounds (1-200000 tokens, 1-600000ms, 1-10 attempts) or not a whole number.
|
|
75
|
+
- Deliberately left as fixed policy, not exposed per provider: the LangGraph loop's tool-call step ceiling (`maxSteps`) and repetition guard (`maxRepeats`), and the theme generator's candidate-count bounds/image-generation dimensions — these are product/orchestration decisions, not a fact about which model an admin chose.
|
|
76
|
+
|
|
77
|
+
## 0.5.0
|
|
78
|
+
|
|
79
|
+
### Minor Changes
|
|
80
|
+
|
|
81
|
+
- 5c5ffbd: L21 task 2 — a runtime template + personalisation system for the admin's
|
|
82
|
+
own interface, the counterpart `packages/admin/src/routes/appearance.tsx`
|
|
83
|
+
already gave the public site (contract D) but the admin itself never had:
|
|
84
|
+
before this, `theme.css` was a single hard-coded design with no selector
|
|
85
|
+
and no override mechanism at all.
|
|
86
|
+
|
|
87
|
+
**`@cogenta/core`:** two new error codes, `ADMIN_THEME_TEMPLATE_UNKNOWN`
|
|
88
|
+
and `ADMIN_THEME_INVALID`.
|
|
89
|
+
|
|
90
|
+
**`@cogenta/schema`:** a new `admin-theme-templates.ts` — two complete,
|
|
91
|
+
built-in token sets (`ADMIN_THEME_TEMPLATES`): "Nightops" (the current
|
|
92
|
+
dark-first, signal-green console — copied verbatim from `theme.css`) and
|
|
93
|
+
"Atelier" (the warm, printed-paper design that shipped immediately before
|
|
94
|
+
the Nightops reskin, recovered from git history rather than approximated
|
|
95
|
+
from memory) — plus `adminThemeOverridesSchema`, the small, curated set of
|
|
96
|
+
personalisation levers a template can be customised with (primary/
|
|
97
|
+
background/text colour, display font, body font, corner radius, an
|
|
98
|
+
optional logo media id) without ever rewriting the built-in template
|
|
99
|
+
itself. `ensureAdminThemeTable`/`createAdminThemeStore` persist exactly one
|
|
100
|
+
choice (a template id plus its overrides) in a new fixed table
|
|
101
|
+
(`cogenta_admin_theme`, the same one-table-no-migration-file treatment
|
|
102
|
+
`menu-tables.ts`/`site-settings-tables.ts` already use for admin-editable,
|
|
103
|
+
non-schema-declared state).
|
|
104
|
+
|
|
105
|
+
**`@cogenta/api`:** `createAdminThemeRouter` — `GET|PUT /api/admin-theme`.
|
|
106
|
+
Read needs no session at all (the admin's own `/login` screen has to paint
|
|
107
|
+
in the chosen template before one exists); write needs the `admin` role,
|
|
108
|
+
checked by the router itself.
|
|
109
|
+
|
|
110
|
+
**`@cogenta/cli`:** `cogenta serve` mounts the new store and router, and
|
|
111
|
+
audits every successful `PUT` the same way `/api/settings` already does.
|
|
112
|
+
|
|
113
|
+
No breaking changes — a site that never calls `PUT /api/admin-theme` keeps
|
|
114
|
+
`theme.css`'s own "Nightops" defaults exactly as before. `@cogenta/admin`
|
|
115
|
+
(private, no changeset) gains the settings screen ("Apparence de l'admin",
|
|
116
|
+
deliberately a separate nav entry from the public site's own "Apparence"),
|
|
117
|
+
`AdminThemeProvider` (injects the computed CSS as a `<style>` tag,
|
|
118
|
+
cascading over `theme.css`'s own tokens), and a personalised logo in the
|
|
119
|
+
top bar when one is set.
|
|
120
|
+
- 0e88f30: L22 task 1/1bis: the agent runtime is real. `AgentRegistry` used to only enable/disable a fixed, in-memory declaration array — nothing ever executed. `@cogenta/agents` gains a real execution loop wiring (`createAgentRunner`, `agents/orchestrator.ts`) together with everything the loop needed but never had a home for: persistent, editable agent declarations (`createFileAgentDeclarationStore`), a persistent, encrypted-at-rest LLM provider store (`createFileProviderConfigStore`, AES-256-GCM keyed from `COGENTA_AUTH_SIGNING_KEY`, R7), and a "skills" instruction-text library (`createFileAgentSkillStore`, `skills/library.ts` — deliberately distinct from L7's marketplace skill registry). Three built-ins are seeded on first boot: the superagent ("Cogenta Agent", enabled by default, autonomy `propose`) and two disabled examples (a dependency-scanner agent backed by the new `deps.scan` tool, and a content-watch example). Autonomy has a new three-level UI mapping (`report-only`/`co-pilot`/`autopilot`) onto contract C's frozen `AutonomyLevel` vocabulary (`autonomy/levels.ts`) — the contract itself is unchanged.
|
|
121
|
+
|
|
122
|
+
`@cogenta/api`'s `agents-router.ts` gains real `create`/`update`/`remove`/`run` capabilities (all optional on `AgentRegistryLike`, backward compatible with a caller that only ever built a fixed `createAgentRegistry`); two new routers, `providers-router.ts` and `agent-skills-router.ts`. All three routers now correctly `decodeURIComponent` path segments — a pre-existing gap in `agents-router.ts` this lot's own end-to-end test caught (the seeded superagent's name, "Cogenta Agent", contains a space). New `ErrorCode`s (`@cogenta/core`): `AGENT_DUPLICATE`, `AGENT_DISABLED`, `AGENT_NO_PROVIDER` (501, mirrors `SITE_PLAN_NO_PROVIDER` — R2's "no provider configured" is not a failure), `AGENT_BUILTIN_UNDELETABLE`, `PROVIDER_NOT_CONFIGURED`, `AGENT_SKILL_UNKNOWN`/`AGENT_SKILL_DUPLICATE`/`AGENT_SKILL_BUILTIN_UNDELETABLE`, `AGENT_REGISTRY_READ_ONLY` (501), `AGENT_RUNTIME_UNAVAILABLE` (503, mirrors `ASSIST_UNAVAILABLE`).
|
|
123
|
+
|
|
124
|
+
**Breaking, within pre-alpha's existing minor-only convention** (see prior changesets' own note): `createAgentDelegateTool`'s tool name is no longer the fixed `agent.delegate` — it is now `agent.delegate.<slug-of-subagent-name>`, so an orchestrator offering several named sub-agents can expose each as a distinct, nameable tool instead of one ambiguous generic call. The permission stays the single, taxonomy-fixed `agent.delegate` (`tools@1.0`); no contract change.
|
|
125
|
+
|
|
126
|
+
`@cogenta/cli`'s `cogenta serve` now always constructs this runtime (three file stores under `.cogenta/agents-runtime/`) and mounts `/api/agents`, `/api/providers` and `/api/agent-skills` unconditionally — R2 still holds: without a configured provider, every route above works except `POST /api/agents/:name/run`, which refuses with `AGENT_NO_PROVIDER` before any network call (proven end to end in `packages/cli/test/serve-agents.test.ts`, including a real tool-calling loop and an R4 permission-refusal case against a local HTTP double of the Anthropic Messages API). `create-cogenta` seeds the same three built-ins at scaffold time.
|
|
127
|
+
|
|
128
|
+
The admin's "Agents" screen (`packages/admin`, private, no changeset) is genuinely editable now — create/edit/run/delete a sub-agent, per-tool permission checklist, autonomy/budget/skills/sub-agents — and gains two new screens, "Providers" and "Skills".
|
|
129
|
+
- c489fde: API keys gain the last two lifecycle actions fiche 20 left open: purge and
|
|
130
|
+
recovery from a mistaken revocation (fiche 62).
|
|
131
|
+
|
|
132
|
+
`ApiKeyStore` (`@cogenta/auth`) gains `purge(id)` — a real, permanent
|
|
133
|
+
`DELETE` of the key row and its usage history, refused unless the key has
|
|
134
|
+
been revoked for at least `MIN_PURGE_AFTER_REVOKED_DAYS` (30, newly exported)
|
|
135
|
+
— and `recover(id)` — mints a replacement carrying the same name, scope and
|
|
136
|
+
quota as a key revoked by mistake, without ever lifting that key's
|
|
137
|
+
`revokedAt` back to `null`. Recovery only works within
|
|
138
|
+
`RECOVERY_WINDOW_MS` (24h, newly exported) of the revocation; past that
|
|
139
|
+
window, or for a key that was never revoked, both throw the two new error
|
|
140
|
+
codes below. This is decision (b) from fiche 62's own recommendation: a
|
|
141
|
+
revoked key is usually revoked for a security reason, so recovery mints a
|
|
142
|
+
new credential rather than silently reactivating a possibly compromised one.
|
|
143
|
+
|
|
144
|
+
`@cogenta/core` gains two error codes: `API_KEY_PURGE_INVALID` and
|
|
145
|
+
`API_KEY_RECOVERY_INVALID` (both mapped to HTTP 409 — the id names something
|
|
146
|
+
real, refused only because of its current state).
|
|
147
|
+
|
|
148
|
+
`@cogenta/api`'s `/api/api-keys` router gains `DELETE .../purge` and
|
|
149
|
+
`POST .../recover`, both admin-only, following the same request/response
|
|
150
|
+
shape as the existing `rotate` route (the raw key appears exactly once, in
|
|
151
|
+
the `recover` response).
|
|
152
|
+
|
|
153
|
+
`cogenta serve` (`@cogenta/cli`) records `apikey.purge` and `apikey.recover`
|
|
154
|
+
in the audit log, alongside the `apikey.create`/`apikey.rotate`/
|
|
155
|
+
`apikey.revoke` entries that already existed — every API key lifecycle
|
|
156
|
+
mutation now produces a verifiable audit entry, closing the gap fiche 20
|
|
157
|
+
first flagged.
|
|
158
|
+
- 54ca689: API key lifecycle, rotation and a per-key request quota (fiche 20).
|
|
159
|
+
|
|
160
|
+
**Breaking (`@cogenta/api`):** `POST /api/api-keys` no longer mints a key that
|
|
161
|
+
never expires by default. A request that omits `expiresAt` now gets a
|
|
162
|
+
90-day expiry — a real, generous but bounded default, since a key with no
|
|
163
|
+
expiry is a key that leaks forever. Pass `neverExpires: true` explicitly to
|
|
164
|
+
keep the old "never expires" behaviour. Any script that creates API keys
|
|
165
|
+
without setting `expiresAt` will see its keys start expiring after 90 days;
|
|
166
|
+
set `neverExpires: true` (or a longer `expiresAt`) if that is not wanted.
|
|
167
|
+
|
|
168
|
+
New, additive:
|
|
169
|
+
|
|
170
|
+
- `POST /api/api-keys/{id}/rotate` (`@cogenta/api`, `@cogenta/auth`'s
|
|
171
|
+
`ApiKeyStore.rotate`): mints a replacement carrying the same name, scope
|
|
172
|
+
and quota, and lets the original keep authenticating for a chosen grace
|
|
173
|
+
window (1h/24h/7d) instead of dying mid-flight. The new key's raw value is
|
|
174
|
+
returned exactly once, the same rule `POST /api/api-keys` already follows.
|
|
175
|
+
- A per-key request quota (`rateLimitPerMinute`, `@cogenta/auth`), enforced
|
|
176
|
+
once per request by `resolveActor` when a `RateLimitDriver` is supplied.
|
|
177
|
+
Exceeding it answers `429` with `Retry-After` and `RateLimit-*` headers.
|
|
178
|
+
`@cogenta/core` gains the `rateLimit` driver need (`createRateLimitRegistry`,
|
|
179
|
+
a Redis driver and an in-process one — R1: works with no Redis at all) and
|
|
180
|
+
a matching `rateLimit` configuration section; `cogenta serve`/`doctor` wire
|
|
181
|
+
and report it.
|
|
182
|
+
- Aggregated 7- and 30-day call counts per key (`ApiKeyStore.usage`), and a
|
|
183
|
+
new admin notice when a key is within seven days of expiring
|
|
184
|
+
(`createApiKeyExpiryNoticeSource`).
|
|
185
|
+
- `ApiKey` gains `rateLimitPerMinute` and `supersededBy` (set once a key has
|
|
186
|
+
been rotated). `ApiKeyStore` gains `getById`, `rotate` and `usage`.
|
|
187
|
+
|
|
188
|
+
New error codes: `API_KEY_RATE_LIMITED` (429), `API_KEY_ROTATION_INVALID`
|
|
189
|
+
(409 — a revoked or expired key cannot be rotated), `RATE_LIMIT_FAILED`.
|
|
190
|
+
|
|
191
|
+
The property that a raw API key is shown exactly once, never twice, holds
|
|
192
|
+
for the new rotate response too: `listApiKeys` and the `previous` half of a
|
|
193
|
+
rotation response never carry key material.
|
|
194
|
+
- 23299e9: The assistant's vector index is now explained and manageable, not just a raw
|
|
195
|
+
count (L22 task 4).
|
|
196
|
+
|
|
197
|
+
- `GET /api/assistant` now reports, per content collection, whether it is
|
|
198
|
+
included in the index and how many chunks it contributes
|
|
199
|
+
(`vector.collections`), plus the reserved pseudo-collection name reference
|
|
200
|
+
documents are stored under (`vector.referenceCollection`).
|
|
201
|
+
- A new site setting, `assistant.indexedCollections` (`GET|PATCH
|
|
202
|
+
/api/settings`, `admin` only), lets an operator exclude a collection —
|
|
203
|
+
published articles included — from the index. The change is read live: it
|
|
204
|
+
applies on the next content save, with no restart, and the existing
|
|
205
|
+
"Reindex vectors" tool applies it to already-indexed content.
|
|
206
|
+
- A document upload flow — `GET/POST /api/assistant/documents` and `DELETE
|
|
207
|
+
/api/assistant/documents/:id` — lets an admin add reference material (PDF,
|
|
208
|
+
DOCX, Markdown, plain text) to the same index the site's own content feeds,
|
|
209
|
+
reusing the existing `document.extract_text` → `chunkDocument` →
|
|
210
|
+
`EmbeddingProvider.embed` pipeline rather than a second one. Each document
|
|
211
|
+
tracks its own `pending`/`indexed`/`error` state.
|
|
212
|
+
- `@cogenta/agents` gains `createReferenceDocumentStore`,
|
|
213
|
+
`ingestReferenceDocument`/`removeReferenceDocumentVectors`, and the
|
|
214
|
+
`REFERENCE_DOCUMENT_COLLECTION`/`REFERENCE_DOCUMENT_LOCALE`/`REFERENCE_DOCUMENT_STATUS`
|
|
215
|
+
constants a caller needs to retrieve them (e.g. via `assist.chat`'s
|
|
216
|
+
`collections` input).
|
|
217
|
+
- `@cogenta/core` gains one error code, `ASSIST_DOCUMENT_NOT_FOUND` (404).
|
|
218
|
+
|
|
219
|
+
All of this is additive and degrades the same way the rest of L18 does: a
|
|
220
|
+
site with no embeddings provider gets none of it, and every other feature
|
|
221
|
+
works unchanged (R2).
|
|
222
|
+
- 0692713: Fiche 30 — agents and assistant admin:
|
|
223
|
+
|
|
224
|
+
- `@cogenta/core`: adds a resolved `assistant.monthlyTokenLimit` config section (default one million tokens a month) and a new `ASSIST_BUDGET_EXCEEDED` error code.
|
|
225
|
+
- `@cogenta/agents`: adds `createAssistUsageTracker`, a per-tool, calendar-bucketed token/call counter for the writing assistant (distinct from the existing per-agent `BudgetTracker`), wired into `createAssistToolset` and `createAssistRuntime` (`AssistRuntimeOptions.onUsage`, `AssistRequest.tool`). `AssistToolset` gains optional `model` and `usage` fields.
|
|
226
|
+
- `@cogenta/api`: `GET /api/assistant` now reports `model`, `usage` (when a tracker is configured) and `vector` (driver/dimensions/count/lastIndexedAt, when a vector store exists). `POST /api/assistant/run` refuses with `ASSIST_BUDGET_EXCEEDED` (429) once the monthly cap is reached, before the provider is called. `createAssistantRouter` gains an optional `vectorInfo` option.
|
|
227
|
+
- `@cogenta/cli`: `AssistantAssembly` gains `vectorInfo` (vector index visibility) and wires a usage tracker into the assistant toolset from `config.assistant.monthlyTokenLimit`. `withVectorIndexing` gains an optional `onIndexed` callback. `recordContentAudit` now records an accepted assistant suggestion's `field`/`tool` (sent by the admin as `assistApplied` on a content save) distinctly in the audit diff, alongside contract A's existing `provenance`/`provenanceDetail`.
|
|
228
|
+
|
|
229
|
+
All additive — a site with no `assistant` config section gets the same default cap as before, and a site with no AI provider sees no `usage`/`model`/`vector` fields at all.
|
|
230
|
+
- 36744d3: Fiche 21: the audit log gains what the state-of-the-art comparison named as
|
|
231
|
+
missing — a real entry detail, filters that reach a date range, an export,
|
|
232
|
+
an actually-scheduled integrity check, and a way to tell a human's action
|
|
233
|
+
from an agent's.
|
|
234
|
+
|
|
235
|
+
**Task 1 — detail.** `GET /api/audit/{id}` (`@cogenta/api`'s `audit-router.ts`)
|
|
236
|
+
answers with the entry, its resolved actor kind and label (an email, or an
|
|
237
|
+
API key's name), and — for a `content.create`/`update`/`restore` action — the
|
|
238
|
+
same structural diff `GET /{collection}/{id}/diff` already computes, called
|
|
239
|
+
through rather than recomputed (the fiche's own warning against duplicating
|
|
240
|
+
it). This needed a place to keep which content version an action produced:
|
|
241
|
+
`RecordAuditInput`/`AuditEntry` gain `version`, stored in a new nullable
|
|
242
|
+
`cogenta_audit_log.version` column added with a `try`/`catch` `alter table`
|
|
243
|
+
(no portable `add column if not exists` across SQLite/Postgres/MySQL) — and
|
|
244
|
+
**deliberately excluded from the hash `computeHash` chains together**. Adding
|
|
245
|
+
a field to that canonical list would change what every already-recorded hash
|
|
246
|
+
means, and every site's existing chain would fail `verify()` the moment this
|
|
247
|
+
code ran. The fields that matter for accountability — who, when, what
|
|
248
|
+
action, on what — are untouched; `version` is UI-convenience metadata, not
|
|
249
|
+
inside the tamper-evidence boundary. A permission refusal on the diff's own
|
|
250
|
+
collection (an admin who was never granted an authoring role there) degrades
|
|
251
|
+
to `diffUnavailable`, not a 403 for the whole entry.
|
|
252
|
+
|
|
253
|
+
**Task 2 — dates, export, pagination.** `since`/`until`/`actorKind` filters
|
|
254
|
+
on `GET /api/audit`, and `GET /api/audit/export?format=csv|json` (bounded to
|
|
255
|
+
10,000 entries) for the filtered view. The export is itself an audit-worthy
|
|
256
|
+
event — a personal-data extraction, per the fiche — recorded as
|
|
257
|
+
`audit.export` (format and count only, never the exported rows) at the same
|
|
258
|
+
transport-boundary layer `cogenta serve` already records every other
|
|
259
|
+
mutation at.
|
|
260
|
+
|
|
261
|
+
**Task 3 — scheduled integrity, for real.** `@cogenta/auth` gains
|
|
262
|
+
`AuditLog.verifyRange`/`get` (a bounded, checkpoint-resuming form of
|
|
263
|
+
`verify()`) and `createAuditIntegrityStore`, which persists the last
|
|
264
|
+
check's outcome across a restart. `cogenta serve` runs it once at startup
|
|
265
|
+
and then on its own `setInterval` (daily by default,
|
|
266
|
+
`ServeOptions.auditIntegrityTickMs` overridable for tests) — the same
|
|
267
|
+
accepted trade-off as the scheduled-publication tick. Most runs are
|
|
268
|
+
incremental (only entries after the last checkpoint); a full replay runs
|
|
269
|
+
weekly on its own as the backstop the fiche asks for, since an incremental
|
|
270
|
+
check cannot see tampering in already-checkpointed history. A break sends
|
|
271
|
+
one signed channel alert (`security.audit_integrity_broken`, only on the run
|
|
272
|
+
that first finds it — never once per tick) and a non-dismissible, danger-
|
|
273
|
+
severity admin notice that clears itself once a forced full check reports
|
|
274
|
+
the chain intact again. `GET`/`POST /api/audit/integrity` expose the status
|
|
275
|
+
and the "verify now" that persists its result, alongside the untouched,
|
|
276
|
+
stateless `GET /api/audit/verify`.
|
|
277
|
+
|
|
278
|
+
**Task 4 — distinguishing actors.** `classifyAuditActor` (`@cogenta/auth`)
|
|
279
|
+
reads signals the log already carried — `actorId === null` is `system`, the
|
|
280
|
+
`apikey:` prefix `resolveActor` has minted since L13 is `api_key`, the
|
|
281
|
+
`agent.tool.` prefix `withAudit` has minted since L4 is `agent`, everything
|
|
282
|
+
else is `human` — no schema change needed. `withAudit` (`@cogenta/agents`)
|
|
283
|
+
gains optional `model`/`autonomyLevel`, carried into the recorded diff when
|
|
284
|
+
a caller tracks them. `?actorKind=` filters `GET /api/audit`.
|
|
285
|
+
|
|
286
|
+
**Task 5 — retention, honestly.** No purge is wired into a schedule in this
|
|
287
|
+
pass — `AuditLog.prune(olderThan)` exists, tested, and safe (it refuses to
|
|
288
|
+
purge a segment that does not itself verify first, and records a genesis
|
|
289
|
+
anchor so the surviving chain keeps verifying from a documented truncation
|
|
290
|
+
point rather than silently going quiet about it), but nothing calls it
|
|
291
|
+
automatically yet. The admin screen says so plainly: this journal keeps
|
|
292
|
+
every entry and grows without limit until an operator acts.
|
|
293
|
+
|
|
294
|
+
None of this is a breaking change: `AuditLog.verify()`'s signature and every
|
|
295
|
+
existing route's response shape are unchanged, and the new column/tables
|
|
296
|
+
are additive (a fresh `ensureAuthTables` run tolerates them being already
|
|
297
|
+
there, an existing install picks them up the same way).
|
|
298
|
+
- af57fa2: L22 task 2: the inbound side of `@cogenta/channels` (L6) is wired for real. Until now, `@cogenta/channels`' identity-linking protocol, command router, and Telegram/Slack/Discord adapters were built and tested but never connected to anything — only outbound notice delivery was live.
|
|
299
|
+
|
|
300
|
+
`@cogenta/channels` gains a plug-in point `createCommandRouter`'s own header comment named as explicitly out of scope for L6: `CommandRouterOptions.chat`. A message that matches no *registered* command name now falls through to this optional handler instead of `{kind: 'unrecognized'}` — same `authorizeInboundCommand` gate as a named command, evaluated against `chat.requiredRoles`. `createAgentChatBridge` builds the handler itself: it resolves an optional `"@Agent Name: message"` mention (falling back to a configured default agent, with a warning, on an unknown name), calls a structural `AgentRunnerLike.run(name, instruction, trigger?)` — deliberately the same three-argument shape `@cogenta/api`'s `agents-router.ts` already declares, not `@cogenta/agents`' raw options-object `AgentRunner['run']` — and replies with the result, truncated and flattened to fit the existing `NotificationChannelMessage` budget (`REPORT_SCREEN_BUDGET_CHARS`) rather than stretching an ill-fitting type. The one rule this whole module exists to keep: a linked channel identity is authorized against the *Cogenta account's own* roles before the runner is ever called, and defaults to requiring `admin` — the same role `POST /api/agents/:name/run` itself requires, so a channel can never grant more access than the linked account's own standing already would over HTTP (R4). Zero changes to any provider adapter (`telegram`/`slack`/`discord` `inbound.ts`) were needed — all three already call `router.route()` uniformly for every linked-identity message.
|
|
301
|
+
|
|
302
|
+
`@cogenta/cli` gains a new, separate command: `cogenta channels`. Telegram long-polling is safe per replica only with exactly one dedicated process; Slack Socket Mode and Discord Gateway are each inherently a single persistent connection. None of the three ever start inside `cogenta serve` — this command is a standalone, optional, single-instance process (built the same way `cogenta mcp` is: a second independent entry point onto the same database and the same `.cogenta/agents-runtime` agent declarations, never a second copy of either) whose only job is connecting configured channels and routing authorized chat messages to a real agent run. Bot credentials are read from the environment only (`COGENTA_CHANNELS_TELEGRAM_BOT_TOKEN`, `COGENTA_CHANNELS_SLACK_BOT_TOKEN`/`COGENTA_CHANNELS_SLACK_APP_TOKEN`, `COGENTA_CHANNELS_DISCORD_BOT_TOKEN`) and never written to `cogenta.config.mjs` (R7); a provider with no token configured is simply not started (R1). `buildAgentRuntime`'s options gain an injectable `approvalQueue` (defaulting to a fresh in-memory one, unchanged behaviour for every existing caller), and `AgentRuntimeAssembly` now exposes the live instance it actually uses — every side-effecting core tool (`content.write_draft` included) is `reversible: false`, so `with-autonomy.ts`'s `forcedApproval` always routes it through this queue regardless of autonomy level; exposing it is what let this lot's own end-to-end test (`packages/cli/test/channels-chat.test.ts`) prove a chat message drives the *correct* contract-C tool through to a real approved, created entry, rather than only proving a tool was proposed.
|
|
303
|
+
|
|
304
|
+
The admin (`@cogenta/admin`, private, no changeset) gains a "Canaux" screen in the IA section (any signed-in role, since linking is personal and used for notices too) — reusing the exact `/api/notices/channels/*` linking endpoints fiche 38 already exposed, no new linking mechanism — and extends the existing "MCP" screen with a "Chat API" key purpose: generates an `admin`-scoped API key (the same mechanism `/api/api-keys` already provides) and documents the `POST /api/agents/:name/run` request/response format, the same single-call-per-turn shape the admin's own new floating chat widget (bottom-right, on every authenticated screen) uses via the existing `runAgent` client function — no second streaming protocol.
|
|
305
|
+
|
|
306
|
+
`@cogenta/core` gains one `ErrorCode`: `CHANNEL_PROVIDER_NOT_CONFIGURED`, thrown by `cogenta channels` when a provider's required environment variables are absent (caught internally and logged as "skipped", never surfaced as a failure — R1).
|
|
307
|
+
|
|
308
|
+
Left honestly open: the approval queue `cogenta channels` (and any future admin approvals screen) would decide a pending write against has no REST surface yet — a real, pre-existing gap this lot's own test works around directly rather than papering over, not something to fix here.
|
|
309
|
+
- 322d1a3: Fiche 52 — Cogenta Commerce: orders and customers, the trailing gap this
|
|
310
|
+
audit's own opening line named ("le modèle de commande n'a aucune adresse
|
|
311
|
+
postale structurée"). **Contains a breaking change**, called out below —
|
|
312
|
+
contract E is deliberately not yet frozen (ADR-0024), so this ships as
|
|
313
|
+
`minor` rather than `major` per this project's existing pre-alpha practice
|
|
314
|
+
(see the taxonomies/trash changesets), with the break stated plainly here.
|
|
315
|
+
|
|
316
|
+
**Breaking**: `POST /api/commerce/payments/{id}/refund` now requires a
|
|
317
|
+
non-empty `reason` in the request body ("motif obligatoire" — task 6) and
|
|
318
|
+
answers `{ refund, creditNote }` instead of the bare refund record. A caller
|
|
319
|
+
sending no reason now gets `400 COMMERCE_AMOUNT_INVALID` instead of a
|
|
320
|
+
refund with no stated cause.
|
|
321
|
+
|
|
322
|
+
`@cogenta/commerce`:
|
|
323
|
+
- `Order` gains six structured shipping-address fields
|
|
324
|
+
(`shippingAddressLine1/2`, `shippingCity`, `shippingPostalCode`,
|
|
325
|
+
`shippingRecipient`, `shippingPhone`) and four shipment-tracking fields
|
|
326
|
+
(`trackingCarrier`, `trackingNumber`, `trackingUrl`, `shippedAt`) — all
|
|
327
|
+
nullable, added in place to `cogenta_commerce_orders` on an
|
|
328
|
+
already-deployed site (idempotent `alter table`, same idiom as
|
|
329
|
+
`menu-tables.ts`'s `location` column; no down path exists or is needed for
|
|
330
|
+
an additive nullable column).
|
|
331
|
+
- `OrderStore` gains `placeManual` (a shopkeeper-entered order — phone,
|
|
332
|
+
trade-show, correction — that opens a real cart and calls `place()`
|
|
333
|
+
internally, never a second placement path), `update` (corrects the e-mail
|
|
334
|
+
and/or address while `pending`; refuses with `COMMERCE_ORDER_LOCKED` once
|
|
335
|
+
paid) and `setTracking` (attaches carrier/number/url; moving a `paid`
|
|
336
|
+
order to `shipped` is a side effect of attaching tracking, not a separate
|
|
337
|
+
step). `OrderListOptions` gains `placedFrom`/`placedTo`.
|
|
338
|
+
- New module `order/notify.ts`: `createOrderEmailQueue`, a persisted,
|
|
339
|
+
retried (`MAX_ATTEMPTS = 5`) transactional e-mail queue built on
|
|
340
|
+
`@cogenta/channels`'s existing `createEmailAdapter` — never a second
|
|
341
|
+
transport. A new direct dependency on `@cogenta/channels` follows (R9:
|
|
342
|
+
reuse over reinvention, same package this project already depends on
|
|
343
|
+
elsewhere).
|
|
344
|
+
- New module `order/csv.ts`: `ordersToCsv`, an RFC 4180 accounting export,
|
|
345
|
+
zero dependency (R9) — one row per order (reference, date, status, email,
|
|
346
|
+
the four summed figures, invoice number when one exists). Decision this
|
|
347
|
+
fiche had to make and is documenting here: row-per-order rather than
|
|
348
|
+
row-per-line, matching the fiche's own singular "export comptable" wording.
|
|
349
|
+
- `CustomerStore` gains `anonymize` (GDPR erasure of the customer record —
|
|
350
|
+
email/name only; an order's own historical copy of the email is
|
|
351
|
+
deliberately retained as a financial record).
|
|
352
|
+
- New module `invoice/credit-note.ts`: `createCreditNoteStore`, one credit
|
|
353
|
+
note per refund (its own `CN-2026` series, sharing the same
|
|
354
|
+
compare-and-set sequence table as invoices via the newly extracted
|
|
355
|
+
`invoice/sequence.ts`) — issued automatically by the refund route once
|
|
356
|
+
billing is configured, never a second manual step.
|
|
357
|
+
- `CommerceAdminRouter` gains routes: `POST/GET/PATCH /orders`,
|
|
358
|
+
`PUT /orders/{id}/tracking`, `GET /orders/{id}/emails`,
|
|
359
|
+
`GET /orders/{id}/credit-notes`, `GET /orders/export.csv`,
|
|
360
|
+
`GET /payments/{id}/refunds`, `GET/POST /customers/{id}`,
|
|
361
|
+
`POST /customers/{id}/export`, `POST /customers/{id}/anonymize`.
|
|
362
|
+
`CommerceResponse.body` can now also be a plain `string` (the CSV export),
|
|
363
|
+
alongside the existing JSON/`Uint8Array` shapes.
|
|
364
|
+
- `@cogenta/core` gains four error codes: `COMMERCE_CUSTOMER_NOT_FOUND`,
|
|
365
|
+
`COMMERCE_ORDER_LOCKED`, `COMMERCE_TRACKING_INVALID`,
|
|
366
|
+
`COMMERCE_CREDIT_NOTE_NOT_FOUND`.
|
|
367
|
+
|
|
368
|
+
`@cogenta/cli` wires all of the above into `cogenta serve`: the order-email
|
|
369
|
+
queue (built whenever an e-mail transport is configured — always, in
|
|
370
|
+
practice, since `runServe` builds the degraded `FileEmailTransport`
|
|
371
|
+
unconditionally) and the credit-note store (built whenever `billing` is
|
|
372
|
+
configured, the same gate invoicing already uses) are passed to
|
|
373
|
+
`createCommerceAdminRouter`; a new scheduled task, `commerce-order-emails`
|
|
374
|
+
(`COMMERCE_EMAIL_TICK_MS = 60_000`, overridable via `commerceEmailTickMs`
|
|
375
|
+
for tests), flushes the retry queue — and is correctly folded into the
|
|
376
|
+
scheduler's own heartbeat interval calculation, a real bug this fiche found
|
|
377
|
+
and fixed (the heartbeat previously only ran as often as the *slowest* of
|
|
378
|
+
the other seven tasks needed, so a fast test override on this one alone
|
|
379
|
+
would never actually fire). The transport layer gains a `text/csv` branch
|
|
380
|
+
alongside the existing JSON/PDF ones.
|
|
381
|
+
- 0ca8a79: Add optimistic concurrency detection and per-field error naming for the entry editor (fiche 02, tasks 3 and 7).
|
|
382
|
+
|
|
383
|
+
- `@cogenta/core` gains the `CONTENT_STALE_WRITE` error code.
|
|
384
|
+
- `@cogenta/schema`'s `UpdateInput` gains an optional `expectedUpdatedAt`. When a caller
|
|
385
|
+
passes it, `update()` compares it against the live row's `updatedAt` and refuses with
|
|
386
|
+
`CONTENT_STALE_WRITE` (409) if someone else's write landed first, instead of silently
|
|
387
|
+
overwriting it. Omitting it keeps the previous last-write-wins behaviour unchanged.
|
|
388
|
+
- `@cogenta/api`'s `PATCH` body accepts the new `expectedUpdatedAt`, and `errorResponse`
|
|
389
|
+
now includes `error.field` for `CONTENT_INVALID`/`CONTENT_SLUG_INVALID` refusals, naming
|
|
390
|
+
the schema-declared field the error is about so a client can drive per-field validation
|
|
391
|
+
UI without parsing the message.
|
|
392
|
+
|
|
393
|
+
Both additions are additive and backward compatible: existing callers that never send
|
|
394
|
+
`expectedUpdatedAt` see no behaviour change, and `error.field` is only ever present for
|
|
395
|
+
the two codes listed above.
|
|
396
|
+
- c392e24: Redirects: 404 log, prefix patterns, editing, CSV import/export, automatic
|
|
397
|
+
redirect on slug rename, and 307/308/410 status codes (fiche 12).
|
|
398
|
+
|
|
399
|
+
**`@cogenta/core`**: gains a `notFoundLog` config section (`enabled`,
|
|
400
|
+
`maxPaths`, `retainDays`) — on by default, bounded, purged past its
|
|
401
|
+
retention. Never stores an IP address or a user agent.
|
|
402
|
+
|
|
403
|
+
**`@cogenta/schema`**:
|
|
404
|
+
- `RedirectStatus` widens from `301 | 302` to `301 | 302 | 307 | 308 | 410`.
|
|
405
|
+
A 410 (Gone) row needs no `to`. Consumers that exhaustively switch on
|
|
406
|
+
`RedirectStatus` — a rare pattern, but a real one — need a case for the
|
|
407
|
+
three new values.
|
|
408
|
+
- `RedirectStore` gains `update(from, { to?, status? })` — implementors of
|
|
409
|
+
the interface (not typical callers) must add it. `RedirectStore.add`'s
|
|
410
|
+
`to` is now optional, required only when `status` is not 410.
|
|
411
|
+
- New: `createNotFoundLogStore`/`NotFoundLogStore` (the 404 log — aggregated
|
|
412
|
+
by path, capped at `maxPaths` distinct paths, no personal data ever) and
|
|
413
|
+
`createRedirectPatternStore`/`RedirectPatternStore` (prefix redirects —
|
|
414
|
+
`/blog/*` to `/actualites/*` — matched by `startsWith`, never a regular
|
|
415
|
+
expression, so the public routing path can never be exposed to
|
|
416
|
+
catastrophic backtracking).
|
|
417
|
+
- New: `withRedirectTracking` — wraps a `ContentStore` so renaming the slug
|
|
418
|
+
of a **published** entry writes a 301 from the old path to the new one on
|
|
419
|
+
its own, reversibly (renaming back makes the redirect disappear), and a
|
|
420
|
+
chain of renames stays flattened to one hop.
|
|
421
|
+
|
|
422
|
+
**`@cogenta/api`**: `redirect-router.ts` gains `PATCH /api/redirects` (edit
|
|
423
|
+
in place), `?q=`/`?limit=`/`?offset=` on the list, `/api/redirects/patterns`
|
|
424
|
+
(prefix redirects), and `/api/redirects/export` / `/api/redirects/import`
|
|
425
|
+
(CSV, always previewed before anything is written — pass `apply: true` to
|
|
426
|
+
commit). New `createNotFoundRouter` (`GET`/`DELETE /api/not-found`). New
|
|
427
|
+
`parseCsv`/`stringifyCsv` — hand-written, zero dependency (R9).
|
|
428
|
+
|
|
429
|
+
**`@cogenta/cli`**: `cogenta serve` mounts `/api/not-found` and the new
|
|
430
|
+
`/api/redirects/*` routes, applies prefix-redirect resolution after the
|
|
431
|
+
exact-match table finds nothing, answers a 410 with no `Location` header,
|
|
432
|
+
records every public GET that matches no route into the 404 log (never for
|
|
433
|
+
`/api/*`), and purges the log past its retention on a daily tick (new
|
|
434
|
+
`ServeOptions.notFoundPurgeTickMs` overrides it, for tests). Renaming the
|
|
435
|
+
slug of a published entry now writes its redirect automatically, wired
|
|
436
|
+
through `withRedirectTracking`.
|
|
437
|
+
- 562c9c1: Add the "Apparence" admin screen (fiche 14) — the CMS's most-differentiating
|
|
438
|
+
feature, AI skin generation, was previously exposed only through the CLI.
|
|
439
|
+
|
|
440
|
+
- `@cogenta/render` gains `mergeSkinTokens` (`SkinTokenOverrides`): overlays a
|
|
441
|
+
partial token tree onto a complete base skin, group by group, key by key.
|
|
442
|
+
- `@cogenta/schema` gains `createThemeStore`/`ensureThemeTable` — one row of
|
|
443
|
+
theme overrides (a partial token overlay, additional CSS, and four identity
|
|
444
|
+
media references), the database half of the two-source-of-truth design
|
|
445
|
+
task 0 settles on: `theme.tokens.json` stays the versioned file default,
|
|
446
|
+
the database holds what an `admin` changed from the admin screen.
|
|
447
|
+
- `@cogenta/plugins`'s `SkinGalleryEntry` now carries the accepted skin's real
|
|
448
|
+
`tokens` (`null` for a rejected entry) — needed to render a swatch or apply
|
|
449
|
+
a gallery skin, previously only metadata.
|
|
450
|
+
- `@cogenta/api` gains `createThemeRouter` (`GET/PUT/DELETE /api/theme[/overrides]`,
|
|
451
|
+
`GET /api/theme/skins`, `POST /api/theme/skins/:id/apply`,
|
|
452
|
+
`POST /api/theme/generate`, `POST /api/theme/export`), plus the
|
|
453
|
+
`SKIN_*`/`THEME_*` error-code → HTTP-status mappings it needs.
|
|
454
|
+
- `@cogenta/cli` wires it all into `cogenta serve`/`dev`: `resolveStyles()`
|
|
455
|
+
recomputes the served stylesheet on every request (file tokens merged with
|
|
456
|
+
saved overrides plus additional CSS), which is what makes a saved change
|
|
457
|
+
visible on the very next page view instead of only after a restart — the
|
|
458
|
+
"hot swap" contract D already promised for the file alone. A new
|
|
459
|
+
`POST /api/theme/preview` route renders the real home page with a candidate
|
|
460
|
+
overlay nobody has saved yet, the same iframe-on-the-real-render decision
|
|
461
|
+
L16 made for the page builder. Exporting the merged tokens back into
|
|
462
|
+
`theme.tokens.json` is gated to `cogenta dev` only, mirroring the
|
|
463
|
+
ADR-0010 rule L19's site-plan applier already uses for the schema file.
|
|
464
|
+
|
|
465
|
+
R2 verified: without an LLM provider, `GET /api/theme` reports
|
|
466
|
+
`aiAvailable: false` and the admin's AI section does not render at all — no
|
|
467
|
+
error, no dead link. R6 verified: an AI-generated candidate or a chosen
|
|
468
|
+
gallery skin is never applied automatically; a save is always a separate,
|
|
469
|
+
explicit action.
|
|
470
|
+
- edf5623: Fiche 15 — comments (ADR-0025, new contract F, `comments@1.0`):
|
|
471
|
+
|
|
472
|
+
- **New package `@cogenta/comments`**: the comment model and store
|
|
473
|
+
(`CommentStore`) — plain-text body only (R3: no HTML tags accepted, ever),
|
|
474
|
+
hashed IP (never stored in clear, RGPD), moderation status
|
|
475
|
+
(`pending`/`approved`/`spam`/`trash`), threading via `parentId`,
|
|
476
|
+
`provenance`. A reversible migration (`ensureCommentsTables`/
|
|
477
|
+
`dropCommentsTables`), tested up/down/up on SQLite; Postgres/MySQL/MariaDB
|
|
478
|
+
integration tests are written (`test/integration/tables.test.ts`) but not
|
|
479
|
+
executed this session (no local Docker). `createCommentsRouter` is the
|
|
480
|
+
CMS's first public write route (`POST /api/comments`, no actor required)
|
|
481
|
+
plus the admin moderation queue, both behind contract F's own permission
|
|
482
|
+
vocabulary (`comments.read`/`moderate`/`reply`/`purge`/`settings`, distinct
|
|
483
|
+
from contract A's five frozen actions). The public route enforces, from
|
|
484
|
+
day one: rate limiting by IP and by target (`createCommentRateLimiter`),
|
|
485
|
+
a honeypot field, a minimum fill-delay, non-AI spam heuristics
|
|
486
|
+
(`checkSpamHeuristics`), and the WordPress "auto-approve a returning
|
|
487
|
+
commenter" rule. A no-JS `<form method=post>` gets a `303` redirect back to
|
|
488
|
+
its own page (`redirectTo`, validated against open-redirect and HTTP
|
|
489
|
+
response-splitting) instead of a raw JSON body.
|
|
490
|
+
- **`@cogenta/core`**: ten new error codes (`COMMENT_NOT_FOUND`,
|
|
491
|
+
`COMMENT_BODY_INVALID`, `COMMENT_AUTHOR_INVALID`, `COMMENT_TARGET_INVALID`,
|
|
492
|
+
`COMMENT_TARGET_CLOSED`, `COMMENT_PARENT_INVALID`,
|
|
493
|
+
`COMMENT_PARENT_TOO_DEEP`, `COMMENT_STATUS_INVALID`,
|
|
494
|
+
`COMMENT_RATE_LIMITED`, `COMMENT_SPAM_DETECTED`).
|
|
495
|
+
- **`@cogenta/schema`**: `SITE_SETTINGS_REGISTRY` gains the `discussion`
|
|
496
|
+
group (`discussion.enabled`/`moderationRequired`/`allowAnonymous`/
|
|
497
|
+
`autoCloseDays`/`maxNestingDepth`/`notifyEmail`) — the site-wide defaults
|
|
498
|
+
a collection or an entry can still override from `@cogenta/comments`'s own
|
|
499
|
+
settings store (per-collection/per-entry overrides deliberately do not
|
|
500
|
+
live in this registry, which is site/locale scoped only).
|
|
501
|
+
- **`@cogenta/api`**: `shell-status-router.ts` gains `commentsPending` (a
|
|
502
|
+
structural `CommentsQueueLike`, the same pattern `commerceOrdersPending`
|
|
503
|
+
already uses) — additive, existing callers that never pass `comments` see
|
|
504
|
+
`null` exactly as before.
|
|
505
|
+
- **`@cogenta/theme-canonical`**: `renderCommentsSection` — the comment
|
|
506
|
+
thread and its plain-HTML submission form, built through the existing
|
|
507
|
+
`h()`/`text()` tree (no `raw()` escape hatch exists in this package, which
|
|
508
|
+
is what makes "no visitor HTML ever reaches the page" structural rather
|
|
509
|
+
than a habit to remember). Rendered by `renderEntryPage`
|
|
510
|
+
(`@cogenta/cli`'s `theme-render.ts`) after the page's own `<main>`, on both
|
|
511
|
+
the published page and the L16 page-builder preview's own draft render —
|
|
512
|
+
except the preview, which never shows it (its `_ts` anti-spam field cannot
|
|
513
|
+
be identical across two separate renders, so byte-identity there would be
|
|
514
|
+
comparing two different legitimate values; `serve-builder.test.ts`'s
|
|
515
|
+
fidelity test now documents this as a deliberate, checked difference).
|
|
516
|
+
Contract B is untouched — no `comments` block, same reasoning L10 gave for
|
|
517
|
+
`/search`.
|
|
518
|
+
- **`@cogenta/import`**: `importWordPress` gains an optional `comments`
|
|
519
|
+
option (a `CommentStore`) — when given, every importable WordPress comment
|
|
520
|
+
is written with its real status (`wp:comment_approved` mapped to
|
|
521
|
+
pending/approved/spam/trash, not just `'1'`), real threading
|
|
522
|
+
(`wp:comment_parent`), on **both** posts and pages. Pages never imported a
|
|
523
|
+
single comment before this — a real, independent bug, not something this
|
|
524
|
+
fiche introduced, found while checking what the importer does today per
|
|
525
|
+
the fiche's own instruction. Inline HTML a legacy WordPress comment form
|
|
526
|
+
allowed (`<a>`, `<em>`, …) is stripped to plain text and reported (R3: no
|
|
527
|
+
escape hatch). Absent `comments` keeps the pre-fiche-15 behaviour
|
|
528
|
+
unchanged (approved-only, posts-only, the synthetic `comment` collection)
|
|
529
|
+
for a caller that has not wired `@cogenta/comments` yet — its `post` field
|
|
530
|
+
is a hard `relation` to the `post` collection specifically, so extending
|
|
531
|
+
it to pages was never an option, only the real store is.
|
|
532
|
+
- **`@cogenta/cli`**: `cogenta serve` mounts `/api/comments` (public POST +
|
|
533
|
+
moderation queue), extends `readBody` to also parse
|
|
534
|
+
`application/x-www-form-urlencoded` (the no-JS form's own content type —
|
|
535
|
+
every other route still only ever sends JSON), wires the comment thread
|
|
536
|
+
into `theme-render.ts`'s page render, and passes a real `CommentStore`
|
|
537
|
+
into every `importWordPress` call site (the terminal command and the
|
|
538
|
+
admin's import screen alike). `cogenta doctor`/`serve` create contract F's
|
|
539
|
+
tables idempotently, the same way commerce's tables are created — a site
|
|
540
|
+
that never receives a comment never pays for them.
|
|
541
|
+
|
|
542
|
+
Admin (`@cogenta/admin`, private, no changeset): a moderation queue screen
|
|
543
|
+
(`/comments`, counters, bulk actions, search, reply-from-the-admin), a
|
|
544
|
+
pending-count nav badge, `assist.moderate` reused verbatim as an indicator
|
|
545
|
+
(never an action — its own closed `none`/`review` union already guarantees
|
|
546
|
+
that, per the fiche's own instruction not to build a second decision path),
|
|
547
|
+
a "Discussion" settings tab (previously a placeholder), and a per-entry
|
|
548
|
+
comments toggle in the entry editor sidebar.
|
|
549
|
+
- db307e0: Add form definitions and submissions — contract G (`forms@1.0`, ADR-0026, fiche 16). A site can now build a form in the admin and receive real submissions, without JavaScript and without an AI provider.
|
|
550
|
+
|
|
551
|
+
- New package **`@cogenta/forms`**: `FormDefinition`/`FormSubmission` model (nine field kinds — text, longText, email, phone, number, date, choiceSingle, choiceMulti, consent; no `file` field in this first version, a deliberate scope cut), `createFormStore` (definitions CRUD, `submit`/`list`/`markStatus`/`bulkMarkStatus`/`searchByEmail`/`deleteByEmail`/`purgeExpired`), full server-side `validateSubmission` (independent of any client-side check, for every field kind), anti-abuse primitives (`checkHoneypot`, `checkFillDelay`, `checkSubmitRateLimit`), and `notifyNewSubmission`/`sendAutoresponder` — both built on `@cogenta/channels`'s existing email adapter, never a second transport. `ensureFormsTables` follows the same `create table if not exists` shape as `@cogenta/commerce`'s tables — a site that never builds a form still creates them, since (unlike commerce) forms tables are cheap enough not to gate.
|
|
552
|
+
- `@cogenta/core` gains eleven `FORM_*` error codes.
|
|
553
|
+
- `@cogenta/api` gains `createFormsRouter` (`/api/forms/*`): admin-only CRUD on definitions and submissions (bulk mark/delete, unread count, CSV-ready listing, GDPR search/erase by e-mail), plus the CMS's **second public write route**, `POST /api/forms/{name}/submit` — no actor check, its own defences (honeypot, minimum fill delay, per-IP rate limit, full server-side validation) stand in for one. The client's IP is read from the resolved request context, never from a client-supplied `X-Forwarded-For` header — trusting that header would let an attacker rotate it per request and step around the rate limiter entirely. `ShellStatus` gains `formSubmissionsUnread` for the admin's nav badge (additive).
|
|
554
|
+
- `@cogenta/cli` wires it all into `cogenta serve`: `GET /forms/{name}` is the public, no-JavaScript "route dédiée" ADR-0026 chose over a contract B block (a bloc `form` RFC is left open in parallel); a plain HTML form post is answered with a real redirect on success or an accessible re-display of the visitor's own values and per-field error (`aria-invalid`/`aria-describedby`) on failure; notifications reuse the same `FileEmailTransport` already built for account invitations; submissions past a form's own `retainDays` are purged automatically on a daily tick, the same `retainDays`/`purgeExpired` model ADR-0022 established for the trash.
|
|
555
|
+
- Admin (`@cogenta/admin`, private, no changeset): `routes/forms.tsx` (the builder, reusing fiche 03's `RepeaterField` for the field list rather than a second repeater) and `routes/form-submissions.tsx` (list/filter/detail/bulk actions/CSV export via `lib/csv.ts`/GDPR search & erase by e-mail), with an unread-count nav badge.
|
|
556
|
+
- 49815b9: Account lifecycle: invitation by email, search/pagination/bulk actions, a
|
|
557
|
+
self-service public profile, dormant/MFA-recommended signals, and
|
|
558
|
+
irreversible anonymization (fiche 17).
|
|
559
|
+
|
|
560
|
+
**Breaking (`@cogenta/auth`), in the same pre-1.0 sense the taxonomies/trash
|
|
561
|
+
and redirects changesets already used this bump for**: `User['status']`
|
|
562
|
+
widens from `'active' | 'disabled'` to also include `'invited'` and
|
|
563
|
+
`'anonymized'` — an exhaustive `switch` on the old two-value union needs a
|
|
564
|
+
new case. `User` also gains four new non-optional fields (`displayName`,
|
|
565
|
+
`avatarMediaId`, `bio`, `locale`, all `string | null`) — code that builds a
|
|
566
|
+
`User` object literal by hand (rather than reading one back from
|
|
567
|
+
`UserStore`) needs to add them. `CreateUserInput` gains an optional `status`
|
|
568
|
+
(defaults to `active`, so existing callers are unaffected).
|
|
569
|
+
|
|
570
|
+
**`@cogenta/auth`**:
|
|
571
|
+
- `UserStore` gains `updateProfile` (self-service, fiche 17 task 3),
|
|
572
|
+
`delete` (real hard delete — safe only for a never-accepted `invited`
|
|
573
|
+
account, see its doc comment for why that does not contradict "accounts
|
|
574
|
+
are disabled, never removed"), and `anonymize` (RGPD-erasure: replaces the
|
|
575
|
+
email with a non-reversible `@anonymized.invalid` token, clears the
|
|
576
|
+
profile fields, sets `status: 'anonymized'`).
|
|
577
|
+
- `SessionStore` gains `lastSeenByUser()` — the last activity timestamp for
|
|
578
|
+
every account in one query, across every session ever held (revoked and
|
|
579
|
+
expired included), for the "last sign-in" column and the dormant-account
|
|
580
|
+
signal.
|
|
581
|
+
- `PasswordResetStore` gains `pending(userId)` — the still-usable token for
|
|
582
|
+
a user, if any, without ever returning the token itself. Used by fiche
|
|
583
|
+
17's invitation to answer "invitation sent on …" and to support resend.
|
|
584
|
+
- New table columns on `cogenta_users` (`display_name`, `avatar_media_id`,
|
|
585
|
+
`bio`, `locale`), added the same additive, catch-and-ignore way the API
|
|
586
|
+
key lifecycle columns were.
|
|
587
|
+
- New error codes: `AUTH_INVITE_UNAVAILABLE` (503), `AUTH_INVITE_INVALID_STATE`
|
|
588
|
+
(409), `AUTH_ACCOUNT_ANONYMIZED` (409), `AUTH_ANONYMIZE_CONFIRMATION_MISMATCH`
|
|
589
|
+
(400).
|
|
590
|
+
|
|
591
|
+
**`@cogenta/api`**: `users-router.ts` grows substantially, entirely additive
|
|
592
|
+
at the route level —
|
|
593
|
+
- `POST /api/users` accepts `invite: true`. With `onInvite` wired, it
|
|
594
|
+
creates an `invited` account and hands the invitation token to the
|
|
595
|
+
callback instead of returning a password — the same single-use token
|
|
596
|
+
primitive `/forgot-password` already uses, reused rather than
|
|
597
|
+
reimplemented. Without `onInvite` wired (or the flag omitted), the route
|
|
598
|
+
behaves exactly as it always has: a generated password, shown once (R1's
|
|
599
|
+
mandatory fallback). The response gains `invited`/`emailSent` alongside
|
|
600
|
+
the (now optional) `password`.
|
|
601
|
+
- `GET /api/users` gains `?sort=`, `?after=`, `?limit=`, and a substring
|
|
602
|
+
match on display name as well as email for `?q=`. The response gains
|
|
603
|
+
`page: { hasMore, nextCursor }` and `meta: { invitationEmailAvailable }`
|
|
604
|
+
— `data` is unchanged.
|
|
605
|
+
- `POST /api/users/{id}/invite` (resend) and `DELETE .../invite` (cancel —
|
|
606
|
+
a real delete, safe for the reason above) are new.
|
|
607
|
+
- `POST /api/users/bulk` (`disable`/`enable`/`setRoles` over several ids at
|
|
608
|
+
once, `Promise.allSettled`, a report naming every failure) is new.
|
|
609
|
+
- `PATCH /api/users/me/profile` (self-only, mirrors the existing
|
|
610
|
+
self-only `/me/password`) is new.
|
|
611
|
+
- `POST /api/users/{id}/anonymize` (admin-only, confirmed by typing the
|
|
612
|
+
account's current email, refuses the last active admin the same way
|
|
613
|
+
disabling one already did, writes one `user.anonymize` audit entry that
|
|
614
|
+
never carries the erased address) is new.
|
|
615
|
+
- `auth-router.ts`'s `POST /api/auth/reset-password` gains one line: an
|
|
616
|
+
`invited` account is flipped to `active` the moment its token is
|
|
617
|
+
redeemed — the only place in the product that changes that bit, and the
|
|
618
|
+
reason the invitation never needed a second token type.
|
|
619
|
+
- `statusFor()` gains the four new codes above.
|
|
620
|
+
|
|
621
|
+
**`@cogenta/cli`**: `cogenta serve` wires the users router's `collections`
|
|
622
|
+
(for the MFA-recommended signal) and a new `onInvite` callback, delivered
|
|
623
|
+
through a new `invite-mail.ts` (the file-transport email, sibling to the
|
|
624
|
+
existing `reset-mail.ts`) pointed at the same `/admin/reset-password` screen
|
|
625
|
+
`onForgotPassword` already uses — accepting an invitation and resetting a
|
|
626
|
+
forgotten password redeem the identical token type.
|
|
627
|
+
|
|
628
|
+
Tests: `@cogenta/auth` 189 (19 new), `@cogenta/api` 582 (78 new across
|
|
629
|
+
`users-router.test.ts` and `auth-router.test.ts`), `@cogenta/cli` 236 (11
|
|
630
|
+
new in `test/serve-users.test.ts`, end to end over real HTTP against a real
|
|
631
|
+
mail directory — invite, read the mail, redeem, sign in; single-use and
|
|
632
|
+
expiry; resend/cancel; bulk actions; self-service profile; anonymization
|
|
633
|
+
with audit-log coherence). `@cogenta/admin` (private, no changeset) gains
|
|
634
|
+
26 new UI tests across `test/users/users.test.tsx` and
|
|
635
|
+
`test/users/profile.test.tsx`.
|
|
636
|
+
- 122da7a: Fiche 18 (profile and authentication): TOTP recovery codes, readable sessions
|
|
637
|
+
with bulk sign-out, an account's own activity feed, and a fetchable password
|
|
638
|
+
policy.
|
|
639
|
+
|
|
640
|
+
**`@cogenta/core`** gains two error codes: `AUTH_RECOVERY_CODE_INVALID` and
|
|
641
|
+
`AUTH_RECOVERY_CODES_UNAVAILABLE`.
|
|
642
|
+
|
|
643
|
+
**`@cogenta/auth`** (the priority of this fiche): confirming TOTP enrolment
|
|
644
|
+
now mints ten single-use recovery codes in the same step and hands them back
|
|
645
|
+
— `confirmTotpEnrolment` returns `Promise<RecoveryCodesIssued>` instead of
|
|
646
|
+
`Promise<void>`. New `AuthService` methods: `recoveryCodeLogin`,
|
|
647
|
+
`regenerateRecoveryCodes`, `recoveryCodesStatus`. `passwordLogin`, `totpLogin`
|
|
648
|
+
and `completeWebAuthnLogin` accept an optional `LoginContext` (`userAgent`,
|
|
649
|
+
`ttlMs`) for "remember me" and readable sessions. `SessionStore` gains
|
|
650
|
+
`revokeAllExcept` ("sign out everywhere else") and every session now reports
|
|
651
|
+
a `browser`/`device` pair distilled from the `User-Agent` at creation —
|
|
652
|
+
never the raw header, never an IP address. `CredentialStore` gains
|
|
653
|
+
`setRecoveryCodes`/`recoveryCodesStatus`/`consumeRecoveryCode`/`removeRecoveryCodes`.
|
|
654
|
+
New exports: `generateRecoveryCodes`, `hashRecoveryCode`, `verifyRecoveryCode`,
|
|
655
|
+
`normaliseRecoveryCode`, `RECOVERY_CODE_COUNT`, `parseUserAgent`,
|
|
656
|
+
`ParsedUserAgent`, `LoginContext`, `RecoveryCodesIssued`. Consumption is a
|
|
657
|
+
real compare-and-set on the stored batch (the same idiom `resets.ts` already
|
|
658
|
+
used for password-reset tokens), with a bounded retry against the fresher row
|
|
659
|
+
on a lost race — proven under genuine two-connection SQLite concurrency, code
|
|
660
|
+
by code, in `packages/auth/test/recovery-code-concurrency.test.ts`, alongside
|
|
661
|
+
a naive-control test showing the read-then-write shape it replaces really
|
|
662
|
+
would let one code work twice.
|
|
663
|
+
|
|
664
|
+
**Breaking, honestly**: `confirmTotpEnrolment`'s return type change and the
|
|
665
|
+
new required members on `SessionStore`/`CredentialStore` are real breaks for
|
|
666
|
+
anyone who type-pinned the old signatures or hand-rolled an implementation of
|
|
667
|
+
either store interface — real callers of `createAuthStore`/`createAuthService`
|
|
668
|
+
(the only supported way to get one) are unaffected. Marked `minor` rather than
|
|
669
|
+
`major` per this project's existing 0.x convention (no package has used
|
|
670
|
+
`major` yet, and one now would jump straight to `1.0.0`, which contradicts
|
|
671
|
+
"pre-alpha") — human judgement invited to confirm.
|
|
672
|
+
|
|
673
|
+
**`@cogenta/api`**: new routes `POST /api/auth/recovery-code`,
|
|
674
|
+
`GET /api/auth/password-policy`, `GET /api/auth/totp/recovery-codes`,
|
|
675
|
+
`POST /api/auth/totp/recovery-codes/regenerate`, `POST
|
|
676
|
+
/api/users/me/sessions/revoke-others`, and `GET /api/audit/me` (the one audit
|
|
677
|
+
route open to a non-admin — force-scoped server-side to the caller, never a
|
|
678
|
+
client-supplied id). `POST /api/auth/totp/enrol/confirm`'s response gains
|
|
679
|
+
`recoveryCodes`; `GET /api/users/{id}/sessions` entries gain `browser`,
|
|
680
|
+
`device` and `isCurrent`. New export: `createRecoveryCodeUsedNoticeSource`
|
|
681
|
+
(the security notice a recovery-code sign-in triggers).
|
|
682
|
+
|
|
683
|
+
**`@cogenta/cli`**: `cogenta serve` wires all of the above — the new notice
|
|
684
|
+
source is registered, and a recovery-code sign-in is recorded in the audit
|
|
685
|
+
log as `auth.recovery_code_used` instead of the generic `auth.login`.
|
|
686
|
+
- 2fb2101: Add the editorial site settings screen (fiche 23, ADR-0025's third settings
|
|
687
|
+
category between `cogenta.config.mjs` — infrastructure, read-only — and
|
|
688
|
+
`localStorage` — personal preference).
|
|
689
|
+
|
|
690
|
+
- `@cogenta/schema` gains a typed key/value site-settings store
|
|
691
|
+
(`createSiteSettingsStore`) backed by a closed registry: general (title,
|
|
692
|
+
tagline, admin email, time zone, date/time style), reading (home path,
|
|
693
|
+
posts per page), media (max upload size), and privacy (policy path, cookie
|
|
694
|
+
banner). Every setting has a declared scope (site or per-locale), a default,
|
|
695
|
+
and a required permission; writing an undeclared key is refused.
|
|
696
|
+
- `@cogenta/api` gains `createSitePlanRouter`'s sibling `GET|PATCH
|
|
697
|
+
/api/settings` and extends `GET /api/config-status` with `storage`,
|
|
698
|
+
`llm`/`embeddings`/`imageGeneration`/`vector`, and `billingConfigured` —
|
|
699
|
+
never a secret, never a credential.
|
|
700
|
+
- `@cogenta/cli` wires the new store into `cogenta serve`/`dev`, and
|
|
701
|
+
`theme-render.ts` now serves the configured home path instead of always
|
|
702
|
+
falling back to the hardcoded `/home`.
|
|
703
|
+
- `@cogenta/core` adds `SITE_SETTING_UNKNOWN`/`SITE_SETTING_INVALID` and a
|
|
704
|
+
`secret-hygiene` module the settings screen uses to detect a
|
|
705
|
+
`database.url` with embedded credentials, or a `.env` file readable by
|
|
706
|
+
other users on shared hosting.
|
|
707
|
+
- `create-cogenta` now writes the generated `.env` (which holds
|
|
708
|
+
`COGENTA_AUTH_SIGNING_KEY`) with mode `0o600` instead of the default —
|
|
709
|
+
closing the shared-hosting exposure `docs/hebergement-mutualise.md`
|
|
710
|
+
already named as a known gap.
|
|
711
|
+
|
|
712
|
+
The admin's old single-control "Paramètres" screen (the signed-in account's
|
|
713
|
+
own interface language) moves to "My profile"; `/settings` is now the
|
|
714
|
+
site-wide editorial screen.
|
|
715
|
+
- 0e90b32: Add the "Santé" and "Outils" admin screens (fiche 24), maintenance mode, and a bounded server error journal.
|
|
716
|
+
|
|
717
|
+
- `@cogenta/core`: adds `createErrorLog`, a bounded, redacted ring buffer for the last N server errors — the admin's substitute for reading `stdout` on a host with no access to the process.
|
|
718
|
+
- `@cogenta/schema`: adds `createMaintenanceStore`/`ensureMaintenanceTable` (a one-row on/off switch with a visitor-facing message) and exports `reindexAll`/`reindexEntry` from the search indexer, so a full rebuild reuses exactly what the write path already does on save.
|
|
719
|
+
- `@cogenta/api`: adds `createHealthRouter` (`GET /api/health-report` — literally `cogenta doctor`'s own report, over HTTP; migrations status/apply; audit chain integrity; disk usage; the error log; maintenance mode get/set) and `createToolsRouter` (`GET /api/tools`, `POST /api/tools/{id}/run`, `GET /api/tools/runs[/…]` — seven maintenance tools, always queued, never run inline in the request). Adds a `pending-migrations` notice source.
|
|
720
|
+
- `@cogenta/cli`: `cogenta serve` wires all of the above — `runDoctor` reused unchanged, migrations applied only up to the first destructive one (the CLI is named for the rest), the seven tools (purge caches, reindex search/vectors, regenerate image variants, check links, test email, purge expired trash) running through the existing database-queue driver's degraded tier, and a maintenance-mode gate that serves an uncacheable 503 with a wait page to every anonymous visitor while `/api/*` and `/admin*` stay reachable.
|
|
721
|
+
|
|
722
|
+
Purely additive: `createRequestListener`'s new third parameter is optional, and every `AssembleSiteOptions` addition is optional — a caller that builds a `Site` by hand, or does not pass a migrator, keeps working unchanged.
|
|
723
|
+
- d0bfa1d: Add `@cogenta/export`: content export/import (`export@1.0`, NDJSON, permission-aware),
|
|
724
|
+
media archive export (streaming ZIP, references or full bytes), full-site backup and
|
|
725
|
+
restore (`cogenta-backup@1.0`, engine-independent, checksummed, optionally encrypted
|
|
726
|
+
with a passphrase), and GDPR/RGPD personal-data export by email — fiche 26.
|
|
727
|
+
|
|
728
|
+
`@cogenta/core` gains nine error codes (`EXPORT_*`, `BACKUP_*`, `RESTORE_*`) and exports
|
|
729
|
+
`MEDIA_TABLE`, its media table's physical name, so a caller assembling a full-site
|
|
730
|
+
backup can name every table without depending on `@cogenta/core`'s internals.
|
|
731
|
+
|
|
732
|
+
`@cogenta/cli` gains four new commands: `cogenta export`, `cogenta import content`,
|
|
733
|
+
`cogenta backup create|list`, and `cogenta restore preview|apply`. Restoring a full
|
|
734
|
+
backup is **CLI-only, by design** — it overwrites the database an admin session would
|
|
735
|
+
be running against, so it is never exposed over HTTP; an admin instead applies a
|
|
736
|
+
*content* export (additive, reversible through the trash).
|
|
737
|
+
- 95acedf: Analytics drill-down (fiche 27): pages, referrers, period comparison, custom
|
|
738
|
+
date range, entry-editor stats, CSV export, and configurable, automatically
|
|
739
|
+
purged retention — the gaps found against Jetpack Stats/Plausible/Matomo. No
|
|
740
|
+
new field is collected: every addition is computed from the events row this
|
|
741
|
+
package already wrote (path, referrer domain, device, daily-salted session
|
|
742
|
+
hash), so the site's cookie-free, no-consent-banner posture is unchanged.
|
|
743
|
+
|
|
744
|
+
**`@cogenta/analytics`**: `AnalyticsStore.getSummary` now returns
|
|
745
|
+
`previousTotalViews`/`previousUniqueVisitors`/`viewsChangePercent` — the
|
|
746
|
+
equal-length window immediately before the requested one, with `null` (never
|
|
747
|
+
a misleading `0`) when there is no previous traffic to compare against.
|
|
748
|
+
`getPageStats(path, window)` reports one page's views, previous-period views
|
|
749
|
+
and rank among every path seen in the window — what an entry-editor sidebar
|
|
750
|
+
needs, without pulling the whole top-N list. `purgeEvents(retainDays)` and
|
|
751
|
+
`purgeSalts(retainDays)` delete rows past a configured retention; the events
|
|
752
|
+
table is the largest table on a site with real traffic, and there is no way
|
|
753
|
+
to disable purging outright, only to choose how long to keep.
|
|
754
|
+
|
|
755
|
+
**`@cogenta/core`**: new config section `analytics.retainDays` (default 400
|
|
756
|
+
days), resolved alongside every other site setting.
|
|
757
|
+
|
|
758
|
+
**`@cogenta/api`**: `createAnalyticsRouter`'s `GET /api/analytics/summary`
|
|
759
|
+
accepts a custom `?since=&until=` range (alongside the existing `?days=`),
|
|
760
|
+
reports the period-over-period comparison, and — when the caller wires in
|
|
761
|
+
`resolvePage` — enriches each top page with its entry's title and admin edit
|
|
762
|
+
link. A new `GET /api/analytics/page?path=` answers the same admin-only stats
|
|
763
|
+
for one page. `retainDays`, when wired in, is echoed back as `retentionDays`
|
|
764
|
+
so the admin screen can show a real number instead of a promise.
|
|
765
|
+
|
|
766
|
+
**`@cogenta/cli`**: `cogenta serve` wires the new `analytics.retainDays`
|
|
767
|
+
config into a daily purge tick (same shape as the existing scheduled-publish
|
|
768
|
+
tick — a sweep right away, then one every 24h) and resolves top pages against
|
|
769
|
+
the site's real routes and permission-checked content gateway, so the summary
|
|
770
|
+
screen can link straight to the entry in the admin.
|
|
771
|
+
|
|
772
|
+
Purely additive: a site that never reads `/api/analytics/summary` behaves
|
|
773
|
+
exactly as before.
|
|
774
|
+
- 6e5df34: Fiche 29 — the marketplace gains a real "installed extensions" screen: what
|
|
775
|
+
runs, in which version, with which permissions, and how it's been behaving.
|
|
776
|
+
|
|
777
|
+
**Breaking, in the pre-alpha sense already established for this project (no
|
|
778
|
+
package has ever used `major`, and one would jump straight to `1.0.0`,
|
|
779
|
+
contradicting "pre-alpha"; the breaking shape is called out here instead):**
|
|
780
|
+
`@cogenta/plugins`' `MarketplaceInstallRecord` gains a required `enabled`
|
|
781
|
+
field, and `MarketplacePreview` gains required `engineCompatible`,
|
|
782
|
+
`latestVersion` and `source` fields — anyone constructing these shapes by
|
|
783
|
+
hand (a test double, a custom `MarketplaceInstaller` implementation) needs
|
|
784
|
+
those fields too. `MarketplaceInstaller` gains two new required methods,
|
|
785
|
+
`activate`/`deactivate`, and `uninstall`'s signature grows an optional
|
|
786
|
+
`{ removeData?: boolean }` second argument. `@cogenta/api`'s
|
|
787
|
+
`marketplace-router.ts` mirrors the same shapes structurally, as it always
|
|
788
|
+
has.
|
|
789
|
+
|
|
790
|
+
New, additive:
|
|
791
|
+
|
|
792
|
+
- `@cogenta/plugins`: `createPluginUsageStore` (`permissions/usage.ts`) —
|
|
793
|
+
accumulates real per-run duration, call count, and outcome (ok / error /
|
|
794
|
+
timeout / memory / crash) per plugin, fed by `runPlugin` when given a
|
|
795
|
+
`usageStore` option. `IsolatedRunResult` gains a real, always-present
|
|
796
|
+
`durationMs`. `PluginGrantStore` gains `revokeAll`. The marketplace
|
|
797
|
+
installer gains a manual `enabled` toggle (`activate`/`deactivate`,
|
|
798
|
+
independent of `PluginDisableStore`'s automatic timeout/memory/crash
|
|
799
|
+
disable), an `engineVersion` option that refuses an incompatible install
|
|
800
|
+
or update with the new `MARKETPLACE_ENGINE_INCOMPATIBLE` code (only once a
|
|
801
|
+
caller actually configures a real Cogenta version — the placeholder
|
|
802
|
+
default never fabricates a refusal), and `uninstall(id, { removeData:
|
|
803
|
+
true })`, which also revokes grants and clears the disable/usage records.
|
|
804
|
+
`MarketplaceCatalogEntry` gains an optional `author`, and
|
|
805
|
+
`MarketplaceChangelogEntry` an optional `releasedAt`.
|
|
806
|
+
- `@cogenta/api`: `GET /api/marketplace/installed` (capabilities, disabled
|
|
807
|
+
state, usage, update availability, per item), `GET /api/marketplace/updates`
|
|
808
|
+
and `POST /api/marketplace/updates/apply` (grouped update that always
|
|
809
|
+
skips — never silently applies — anything that would widen permissions),
|
|
810
|
+
`POST /api/marketplace/items/{id}/activate` and `.../deactivate`,
|
|
811
|
+
`POST .../uninstall` now accepts `{ removeData: boolean }` in its body.
|
|
812
|
+
- `@cogenta/core`: new `MARKETPLACE_ENGINE_INCOMPATIBLE` error code, mapped
|
|
813
|
+
to a `422` in `@cogenta/api`'s `statusFor`.
|
|
814
|
+
|
|
815
|
+
Honest limitation, not an oversight: nothing in this repository actually
|
|
816
|
+
calls `runPlugin` yet (no live `AgentRegistry` exists anywhere, the same
|
|
817
|
+
R2-honest gap already noted since L5) — the new usage store is real, tested
|
|
818
|
+
end to end, and wired into `cogenta serve`, but stays empty on a real
|
|
819
|
+
deployment until a real plugin-execution pipeline lands. The installed
|
|
820
|
+
extensions screen says "never run yet" rather than inventing a number.
|
|
821
|
+
- bebbab8: Add store settings for the shop (fiche 34): tax zones/rates with a simulator, shipping
|
|
822
|
+
zones/methods with a simulator, payment driver activation (presence-only for keys, never
|
|
823
|
+
values), general store settings, and a configurable invoice template.
|
|
824
|
+
|
|
825
|
+
- `@cogenta/core` gains a `payment` configuration section (`driver`, `testMode`,
|
|
826
|
+
`manualInstructions`) following the exact `llm`/`billing` pattern: the Stripe secret key
|
|
827
|
+
and webhook secret are never declared in the schema and are refused with
|
|
828
|
+
`CONFIG_SECRET_IN_FILE` if written to `cogenta.config.mjs` — they come only from
|
|
829
|
+
`COGENTA_PAYMENT_STRIPE_SECRET_KEY`/`COGENTA_PAYMENT_STRIPE_WEBHOOK_SECRET`.
|
|
830
|
+
- `@cogenta/schema`'s site-settings registry (fiche 23) gains a `commerce` group
|
|
831
|
+
(currency, tax-inclusive/exclusive display, countries served, minimum order, default
|
|
832
|
+
backorder policy, ToS/return-policy page paths — pointers to real content entries, not
|
|
833
|
+
text fields — and invoice series prefix/payment terms/language) and a new `select`
|
|
834
|
+
`uiType` for closed-choice settings.
|
|
835
|
+
- `@cogenta/commerce`'s admin router gains `GET|POST /tax/rules`, `DELETE
|
|
836
|
+
/tax/rules/{id}`, `POST /tax/simulate` (calls the real resolver, never a second
|
|
837
|
+
implementation), the shipping equivalents (`/shipping/methods`, `/shipping/simulate`),
|
|
838
|
+
and `GET /payment/drivers` / `POST /payment/drivers/{name}/test-connection` (presence
|
|
839
|
+
and live health only, never a key's value). `CommerceAdminRouterOptions` gains required
|
|
840
|
+
`tax`/`shipping` fields and an optional `payment` field — **a breaking change** for any
|
|
841
|
+
direct caller of `createCommerceAdminRouter` that does not yet pass them.
|
|
842
|
+
- `@cogenta/cli`'s `cogenta serve` now selects a real payment gateway through
|
|
843
|
+
`createPaymentRegistry` (Stripe when a key is configured and reachable, bank transfer
|
|
844
|
+
otherwise) instead of a hardcoded manual gateway, and mounts the new commerce settings
|
|
845
|
+
routes.
|
|
846
|
+
- `@cogenta/admin` (private, no changeset) gains four screens under "Boutique": Tax,
|
|
847
|
+
Shipping, Payment, and Store settings (general + invoice template), all `admin`-only.
|
|
848
|
+
|
|
849
|
+
Deliberately not built in this fiche: an inbound `POST /api/commerce/payments/webhook`
|
|
850
|
+
route. `PaymentStore.handleWebhook` is already implemented and tested; wiring it needs
|
|
851
|
+
the raw (non-JSON-parsed) request body, which `cogenta serve`'s shared body reader does
|
|
852
|
+
not yet support for any route. The payment screen shows the webhook URL a deployer would
|
|
853
|
+
configure at Stripe, honestly labelled as not yet receiving events. See `BLOCKERS.md` §15.
|
|
854
|
+
- a8199ea: Media library folders, and the fiche 11 search/filter/sort/pagination/tags/usage/replace
|
|
855
|
+
work — already written and tested, never wired into the admin screen — finally called by
|
|
856
|
+
it (fiche 46).
|
|
857
|
+
|
|
858
|
+
**`@cogenta/core`**: `MediaAsset` gains `folderId: string | null` (`null` means
|
|
859
|
+
unclassified — every asset uploaded before this fiche keeps that value forever, nothing
|
|
860
|
+
backfills it). `CreateMediaInput`/`UpdateMediaInput` gain an optional `folderId`.
|
|
861
|
+
`ListMediaOptions` gains `folderId` (exact match, `null` for unclassified) and
|
|
862
|
+
`folderIds` (an already-resolved set, for "include subfolders"). New: `MediaFolder`,
|
|
863
|
+
`MediaFolderStore`, `createDatabaseMediaFolderStore` — a materialised-path tree
|
|
864
|
+
(same technique as the taxonomy tree, ADR-0022, kept as a *local* copy in
|
|
865
|
+
`folder-path.ts` since `@cogenta/core` cannot depend on `@cogenta/schema`), one
|
|
866
|
+
`cogenta_media_folders` table, folder names unique among siblings. New error codes:
|
|
867
|
+
`MEDIA_FOLDER_NOT_FOUND`, `MEDIA_FOLDER_INVALID`, `MEDIA_FOLDER_NAME_TAKEN`,
|
|
868
|
+
`MEDIA_FOLDER_NOT_EMPTY`, `MEDIA_FOLDER_CYCLE`, `MEDIA_FOLDER_TOO_DEEP`.
|
|
869
|
+
|
|
870
|
+
**`@cogenta/api`**: `media-router.ts` gains `/api/media/folders` (CRUD),
|
|
871
|
+
`/api/media/folders/{id}/move`, `/api/media/{id}/move`, `/api/media/-/bulk-move`, and
|
|
872
|
+
`?folderId=`/`?includeSubfolders=` on `GET /api/media`. `MediaRouterOptions` gains an
|
|
873
|
+
optional `folders?: MediaFolderStore` — absent, the folder routes answer 404 (the same
|
|
874
|
+
graceful-absence shape `usage` already had) and `?folderId=` still works as a plain
|
|
875
|
+
exact match. `STATUS_BY_CODE` gains the six new codes.
|
|
876
|
+
|
|
877
|
+
**`@cogenta/agents`** (no changeset — no observable change): `media.read`/`media.write`
|
|
878
|
+
(contract C) keep exactly the wire output they had before this fiche. `MediaAsset`
|
|
879
|
+
gaining `folderId` would otherwise have grown their shared output schema too — but
|
|
880
|
+
contract C treats an existing tool's signature as figured with no "additive is minor"
|
|
881
|
+
exception (unlike contract A/D, which carry one explicitly), so `folderId` is now
|
|
882
|
+
stripped before that shape is built at all (`toToolAsset`). Exposing it to an agent
|
|
883
|
+
needs a deliberate governance call — a new `tools@1.5` entry permitting additive
|
|
884
|
+
tool-output growth, or a separate tool — left to the human rather than decided here.
|
|
885
|
+
|
|
886
|
+
**`@cogenta/cli`**: `cogenta serve` creates the folder store and bootstraps a default
|
|
887
|
+
`contents` root folder once, idempotently, on every startup; wires `folders` and (a real
|
|
888
|
+
gap found while wiring this fiche's own admin panel — `usage` was written and tested in
|
|
889
|
+
fiche 11 but never actually passed to `createMediaRouter`) `usage` into the media router.
|
|
890
|
+
- 16f63f6: Bring form definitions and submissions closer to parity with premium form plugins (Gravity Forms/WPForms) — fiche 47, tasks 1-4 and 6-11 (task 5, a contract B `form` block, stays out of scope pending its own RFC).
|
|
891
|
+
|
|
892
|
+
- **`@cogenta/forms`**: the field vocabulary gains a tenth kind, **`file`** — a deliberate reopening of ADR-0026's own renoncement, decided live with the user (fiche 47 §8). A `file` field's bytes are sniffed against a closed category vocabulary (`image`/`pdf`/`document`/`text`, via `sniffFormFileCategory`/`assertAllowedFormFile`) — never trusted from a filename or declared `Content-Type` — with a hard, unconfigurable size ceiling (`FORM_FILE_HARD_MAX_BYTES`) on top of any per-field `maxSizeBytes`. `FormFieldDefinition` gains `showIf` (task 1: a field masked by an unmet condition is neither required nor validated, evaluated server-side against the raw submission — `evaluateCondition`/`isFieldVisible`) and `acceptCategories`. `FormDefinition` gains `steps` (task 2: real multi-step forms, validated so every field belongs to exactly one step), `notifyChannels` (task 4: extra Slack/Discord/Telegram/webhook targets via `@cogenta/channels`'s existing `ChannelRegistry`/adapters, never a new transport — `notifyChannels()`) and `captcha` (task 10: optional, off by default, Cloudflare Turnstile verification via `verifyCaptcha`, a single HTTP call, no client SDK dependency). `FormDefinitionStore` gains `duplicate` (task 11: an independent, inactive copy, never carrying submissions over). `FormSubmissionStore` gains `addNote`/`listNotes` (task 8: operator-only notes, never exported) and `list()` gains `query`/`from`/`to` (task 7: full-text search across a submission's own values plus a date range, SQL-filtered then bounded in-memory for the text match — the same honest tradeoff `searchByEmail` already makes). New `csv.ts` (`csvField`/`toCsvRow`/`csvHeaderRow`/`csvSubmissionRow`) mirrors `packages/admin/src/lib/csv.ts`'s CWE-1236 formula-injection guard for the new server-side streamed export (task 9).
|
|
893
|
+
- **`@cogenta/core`**: four new `FORM_*` error codes (`FORM_FILE_REJECTED`, `FORM_CAPTCHA_REQUIRED`, `FORM_CAPTCHA_FAILED`, `FORM_STEP_INVALID`), each mapped to a 4xx status in `@cogenta/api`'s `STATUS_BY_CODE`.
|
|
894
|
+
- **`@cogenta/api`**: `createFormsRouter` gains `storage` (a `StorageDriver`, for the `file` field — absent means every upload is refused rather than silently accepted) and `channelRegistry` options; `POST /api/forms/{name}/submit` now accepts `multipart/form-data` (sniffing and storing any uploaded file before validation), understands multi-step submissions (`_step`/`_accumulated`, answering `202 {status:'step', nextStep, values}` for every step but the last, exactly as before for a single-page form), verifies the CAPTCHA on the final step when a form has one enabled, and dispatches `notifyChannels` alongside the existing e-mail notification. New routes: `POST /api/forms/{id}/duplicate`, `GET`/`POST /api/forms/submissions/{id}/notes`, and `?q=`/`?from=`/`?to=` on `GET /api/forms/submissions`. New export `streamSubmissionsCsv` — an async generator, never buffering the whole export in memory (a single-form export uses that form's own field names as fixed CSV columns; a cross-form export pays one bounded pre-pass to discover columns before streaming rows for real).
|
|
895
|
+
- **`@cogenta/cli`**: `readBody` (`serve.ts`) now parses a real `multipart/form-data` body (reusing `@cogenta/api`'s existing zero-dependency parser) as raw bytes rather than corrupting it through a UTF-8 text decode — this is what makes a `<form enctype="multipart/form-data">` post work with no JavaScript at all, for `/api/forms/*` and (latent, previously dead in production) `/api/media` alike. `forms-page.ts` renders a `file` input, one step at a time for a multi-step form (each step a plain chained `<form method="post">`, no client framework — the original page-load timestamp is carried forward unchanged rather than refreshed, so the anti-abuse fill-delay check keeps its meaning across the whole flow), and the Turnstile widget only on the final step of a form that opted into the CAPTCHA. New route `GET /api/forms/submissions/export.csv` (admin-only, streamed directly to the response, outside `RestResponse`'s JSON-only shape — same reasoning as `/api/media/{id}/file`).
|
|
896
|
+
- Admin (`@cogenta/admin`, private, no changeset): `routes/forms.tsx` gains per-field `showIf`/step/file-category editing (plain text columns on the existing field repeater, not a second visual builder), `notifyChannels`/CAPTCHA configuration, and a Duplicate action; `routes/form-submissions.tsx` gains a search box, a date range filter, internal notes, the referrer (stored since fiche 16 but never shown before), and a server-streamed CSV download (`downloadSubmissionsCsv`) replacing the old 200-row-capped client-side export.
|
|
897
|
+
|
|
898
|
+
A form with none of these features enabled behaves exactly as it did before this change — `steps`/`notifyChannels` default to empty and `captcha` defaults to disabled, and no field's `showIf` means no field's requiredness changed. The form stays fully functional with no JavaScript at every task except the CAPTCHA widget itself, which is opt-in and inherently third-party script.
|
|
899
|
+
- 1dd9e6f: Fiche 53 — coupon per-customer/product limits and a real dunning machine for failed
|
|
900
|
+
subscription renewals, plus the admin screen work pause/resume/billing-history already
|
|
901
|
+
had server-side support for.
|
|
902
|
+
|
|
903
|
+
**Coupons.** `Coupon`/`CreateCouponInput` gain `maxRedemptionsPerCustomer` (on top of,
|
|
904
|
+
never instead of, the existing global `maxRedemptions`) and `restrictedProductIds`
|
|
905
|
+
(commerce product ids; empty means unrestricted). `CouponStore.check()` takes an
|
|
906
|
+
optional fourth `context: { customerId?, productIds? }` argument — existing three-argument
|
|
907
|
+
callers are unaffected — and `CouponCheck` gains two new cases, `customer_exhausted` and
|
|
908
|
+
`not_applicable`; a switch over `CouponCheck.kind` that was exhaustive before this change
|
|
909
|
+
needs a case for both (a real, structural addition to an existing union, called out here
|
|
910
|
+
rather than silently shipped as a patch). `CouponStore.redeem()` now also claims the
|
|
911
|
+
per-customer counter atomically, in the same transaction as the existing global one — a
|
|
912
|
+
customer who loses their own limit's race never burns a global redemption meant for
|
|
913
|
+
someone else. Two new error codes: `COMMERCE_COUPON_CUSTOMER_EXHAUSTED`,
|
|
914
|
+
`COMMERCE_COUPON_NOT_APPLICABLE`. `CouponStore` gains `metrics()`.
|
|
915
|
+
|
|
916
|
+
**Subscriptions — dunning.** `SUBSCRIPTION_STATUSES` gains `past_due` — another
|
|
917
|
+
structural union widening, same caveat as above for an exhaustive switch. A subscription
|
|
918
|
+
lands there the instant a renewal payment fails, and `runBilling`'s own due-query
|
|
919
|
+
(`status = 'active'`) skips it until the cycle resolves. `SubscriptionStore` gains
|
|
920
|
+
`dunning(id)` and `runDunning(options?)`: three retries at 1/3/7 days after the first
|
|
921
|
+
failure by default (the fiche's own proposed calendar, documented as
|
|
922
|
+
`DEFAULT_DUNNING_SCHEDULE_DAYS`, configurable per store via the new
|
|
923
|
+
`SubscriptionStoreOptions.dunningScheduleDays`) — a subscription is never auto-suspended
|
|
924
|
+
before the schedule is exhausted, and `runDunning` replayed on an already-attempted due
|
|
925
|
+
date is a no-op (a compare-and-set on `next_retry_at`, mirroring the scheduler lock in
|
|
926
|
+
`@cogenta/schema`). `pause()`/`resume()`/`cancel()` now also clear an open dunning cycle.
|
|
927
|
+
|
|
928
|
+
**Subscriptions — plan changes.** `SubscriptionStore.changePlan(id, newVariantId,
|
|
929
|
+
options?)` switches the plan immediately with an explicit prorated charge for the rest
|
|
930
|
+
of the current period; a downgrade's credit is reported (`prorationMinor` negative) but
|
|
931
|
+
never silently issued — this store has no credit-note mechanism.
|
|
932
|
+
|
|
933
|
+
**Subscriptions — renewal notices and metrics.** `SubscriptionStoreDependencies` gains
|
|
934
|
+
an optional `notifyRenewal`; `sendRenewalNotices()` is a safe no-op without it (R2). A
|
|
935
|
+
ready-made notifier, `createEmailRenewalNotifier`, is built on `@cogenta/channels`'s own
|
|
936
|
+
`EmailTransport`/`renderEmailMessage` (`@cogenta/commerce` gains a real dependency on
|
|
937
|
+
`@cogenta/channels`) — never a second email renderer. `SubscriptionStore` gains
|
|
938
|
+
`metrics()` (active/past-due/paused/cancelled counts, MRR, churn).
|
|
939
|
+
|
|
940
|
+
**Admin router.** `GET /api/commerce/coupons/metrics`, `GET
|
|
941
|
+
/api/commerce/subscriptions/metrics`, `GET /api/commerce/subscriptions/{id}` (the
|
|
942
|
+
subscription plus its billing history and open dunning cycle), and `POST
|
|
943
|
+
/api/commerce/subscriptions/{id}/change-plan`.
|
|
944
|
+
- 656163e: LLM provider catalog (fiche 56): OpenRouter, DeepSeek, Qwen and GLM are now
|
|
945
|
+
configurable from the admin's "Providers" screen alongside Anthropic, OpenAI
|
|
946
|
+
and Google, plus an explicit "custom provider" option for any other
|
|
947
|
+
OpenAI-compatible endpoint (a self-hosted proxy, or a vendor not yet
|
|
948
|
+
catalogued). No new network code: every OpenAI-compatible entry (OpenRouter,
|
|
949
|
+
DeepSeek, Qwen, GLM, custom) reuses `createOpenAiClient` unmodified, only
|
|
950
|
+
pointed at a different `baseUrl`.
|
|
951
|
+
|
|
952
|
+
**`@cogenta/agents`**: `provider` widens from the closed 3-literal union
|
|
953
|
+
(`'anthropic' | 'openai' | 'google'`) to a plain string, validated at the
|
|
954
|
+
write boundary instead of by a type — `@cogenta/core`'s own
|
|
955
|
+
`llmSchema.provider` was already a free string before this fiche. New
|
|
956
|
+
`providers/catalog.ts`: `KNOWN_PROVIDER_CATALOG` (id/label/wireFormat/
|
|
957
|
+
defaultBaseUrl/knownModels per vendor) and `findProviderCatalogEntry`.
|
|
958
|
+
`createProviderRegistry` resolves a name via the catalog when it knows one
|
|
959
|
+
(dispatching to the right adapter by `wireFormat`), and otherwise requires
|
|
960
|
+
the entry's own `baseUrl` — that pairing (no catalog entry + a `baseUrl`) is
|
|
961
|
+
what "custom provider" means structurally, with no separate flag to keep in
|
|
962
|
+
sync. `createOpenAiClient` gains an optional `name` (defaults to `'openai'`)
|
|
963
|
+
so a client built for OpenRouter/DeepSeek/Qwen/GLM/a custom endpoint reports
|
|
964
|
+
its own id via `ProviderClient.name` — needed for the privacy allowlist
|
|
965
|
+
(`assertProviderAllowed`) to recognise the right vendor rather than every
|
|
966
|
+
OpenAI-compatible client misreporting itself as literally `'openai'`.
|
|
967
|
+
`createFileProviderConfigStore.upsert`/`updateSettings` reject a malformed
|
|
968
|
+
provider id (`PROVIDER_ID_INVALID`) or one outside the catalog with no
|
|
969
|
+
resolvable `baseUrl` (`PROVIDER_CUSTOM_BASE_URL_REQUIRED`) — the write-time
|
|
970
|
+
checks that make network-time resolution failures unreachable.
|
|
971
|
+
|
|
972
|
+
**Breaking (`@cogenta/agents`):** `PROVIDER_NAMES` (the fixed 3-name array)
|
|
973
|
+
is removed — read `KNOWN_PROVIDER_CATALOG` instead, or accept that
|
|
974
|
+
`ProviderName` is now `string`. `ProviderRegistryConfig`'s value shape gains
|
|
975
|
+
nothing new but is now keyed by an open string rather than the closed union.
|
|
976
|
+
|
|
977
|
+
**`@cogenta/core`**: two new error codes, `PROVIDER_ID_INVALID` and
|
|
978
|
+
`PROVIDER_CUSTOM_BASE_URL_REQUIRED` (both 400).
|
|
979
|
+
|
|
980
|
+
**Breaking (`@cogenta/api`):** `providers-router.ts`'s `ProviderRegistryLike`
|
|
981
|
+
gains a required `catalog: readonly ProviderCatalogEntrySummary[]` — any
|
|
982
|
+
caller implementing this interface directly (rather than using
|
|
983
|
+
`@cogenta/cli`'s adapter) must supply it. New route `GET
|
|
984
|
+
/api/providers/catalog` (admin-only) serves it; `catalog` is a reserved
|
|
985
|
+
provider id as a result (a provider literally named "catalog" can no longer
|
|
986
|
+
be created). `POST /api/providers` no longer rejects a provider name outside
|
|
987
|
+
a fixed 3-name list — it rejects a name outside the catalog **only when no
|
|
988
|
+
`baseUrl` is given** (`PROVIDER_CUSTOM_BASE_URL_REQUIRED`, still 400, but a
|
|
989
|
+
different code than the previous generic "not a supported LLM provider"
|
|
990
|
+
`QUERY_INVALID`). `PATCH`/`DELETE /api/providers/:provider` no longer gate
|
|
991
|
+
on a fixed name list at all — they resolve against whatever the store
|
|
992
|
+
actually has saved (a legitimately-saved custom provider used to be
|
|
993
|
+
unreachable by these two verbs; the store's own `PROVIDER_NOT_CONFIGURED`
|
|
994
|
+
already covered "this was never saved").
|
|
995
|
+
|
|
996
|
+
**`@cogenta/cli`**: `packages/cli/src/commands/agent-runtime.ts`'s
|
|
997
|
+
`createProviderRegistryAdapter` now supplies `names`/`catalog` from
|
|
998
|
+
`KNOWN_PROVIDER_CATALOG` instead of the removed `PROVIDER_NAMES`, and no
|
|
999
|
+
longer narrows an arbitrary string against a closed `ProviderName` union
|
|
1000
|
+
before trusting the live registry's own `has`/`get`. `assistant.ts`'s single-
|
|
1001
|
+
provider (`cogenta.config.mjs`'s `llm` section) resolution now accepts any
|
|
1002
|
+
provider `createProviderRegistry` itself can resolve — a catalog id, or a
|
|
1003
|
+
custom id paired with a `baseUrl` — rather than duplicating a fixed 3-name
|
|
1004
|
+
allowlist a second time (the exact desynchronisation risk this repo already
|
|
1005
|
+
hit once with `CONTRACT_C_PERMISSIONS`).
|
|
1006
|
+
|
|
1007
|
+
**`@cogenta/admin`** (unpublished, no changeset entry): the "Providers"
|
|
1008
|
+
screen's "add" form is now catalog-driven — a provider `<select>` populated
|
|
1009
|
+
from `GET /api/providers/catalog`, a known-models picker per selected
|
|
1010
|
+
provider, and an explicit "custom provider" choice (its own id field, and a
|
|
1011
|
+
`baseUrl` the form requires before Save is enabled).
|
|
1012
|
+
|
|
1013
|
+
Not included, by the fiche's own scope: Replicate (a different, asynchronous
|
|
1014
|
+
prediction-and-polling API, not OpenAI-compatible — a separate adapter, left
|
|
1015
|
+
for a later task).
|
|
1016
|
+
- 4513a71: Import gains a real preview/apply/undo flow (fiche 25), on top of the existing
|
|
1017
|
+
one-shot WordPress uploader, which is unchanged and still works.
|
|
1018
|
+
|
|
1019
|
+
`@cogenta/import`:
|
|
1020
|
+
- `analyzeWordPress(xml)` previews a WXR export — counts, proposed collection mapping,
|
|
1021
|
+
authors, media URLs and volume, slug conflicts and everything that will be skipped —
|
|
1022
|
+
without writing anything.
|
|
1023
|
+
- `importWordPress` accepts `{ tracking, runId }`: passed, every post/page/comment it
|
|
1024
|
+
writes is recorded, a second call with the same `runId` resumes without duplicating,
|
|
1025
|
+
and `undoImport` can trash everything the run created (never `purge`, so an
|
|
1026
|
+
over-eager undo is itself reversible from the trash).
|
|
1027
|
+
- New sources: `parseCsv`/`csvToRecords` (zero dependency, RFC 4180), `feedToRecords`
|
|
1028
|
+
(RSS 2.0 and Atom), `parseJsonImport`/`analyzeJson`/`applyJson` (a minimal Cogenta
|
|
1029
|
+
JSON import format). CSV and RSS/Atom share a generic mapping/apply engine
|
|
1030
|
+
(`analyzeGeneric`/`applyGeneric`, `proposeFieldMapping`/`resolveMapping`) against any
|
|
1031
|
+
collection the target site declares — real field correspondence, not a fixed shape.
|
|
1032
|
+
- `createImportTrackingStore` — two new tables (`cogenta_import_runs`/
|
|
1033
|
+
`cogenta_import_items`), owned entirely by this package, never a field on contract A.
|
|
1034
|
+
- Outbound media downloads are now guarded against SSRF (private/loopback/link-local
|
|
1035
|
+
addresses refused, including on a DNS-rebound host name), capped in size and count,
|
|
1036
|
+
and time out.
|
|
1037
|
+
|
|
1038
|
+
`@cogenta/core`: new error codes (`IMPORT_RUN_NOT_FOUND`, `IMPORT_SOURCE_INVALID`,
|
|
1039
|
+
`IMPORT_ALREADY_APPLIED`, `IMPORT_MAPPING_INVALID`, `IMPORT_MEDIA_URL_UNSAFE`,
|
|
1040
|
+
`IMPORT_CSV_INVALID`, `IMPORT_FEED_INVALID`).
|
|
1041
|
+
|
|
1042
|
+
`@cogenta/api`: `createImportRouter` gains `POST /api/import/analyze`,
|
|
1043
|
+
`GET /api/import/runs`, `GET /api/import/runs/{id}`, `POST /api/import/runs/{id}/apply`
|
|
1044
|
+
and `POST /api/import/runs/{id}/cancel`, behind five new optional `ImportRouterOptions`
|
|
1045
|
+
callbacks (`analyze`/`apply`/`getRun`/`listRuns`/`cancel`). All admin-only. The legacy
|
|
1046
|
+
`POST /api/import/wordpress` route is untouched.
|
|
1047
|
+
|
|
1048
|
+
`@cogenta/cli`: `cogenta serve` wires the full flow — WordPress, CSV, JSON and RSS/Atom
|
|
1049
|
+
— through the site's own stores, storage driver and read-only guard.
|
|
1050
|
+
- bdcb563: L20 audit — six real bugs in the admin's diagnostic and dashboard screens,
|
|
1051
|
+
fixed:
|
|
1052
|
+
|
|
1053
|
+
**`@cogenta/core`:** `DriverSelection`/`SkippedDriver` gain `reasonCode`
|
|
1054
|
+
(`DriverSelectionReason`/`SkipReasonCode`) alongside the existing `reason`
|
|
1055
|
+
string — a stable code a translated UI can look up instead of showing
|
|
1056
|
+
`createDriverRegistry`'s English prose ("named in the configuration", "redis
|
|
1057
|
+
not available") verbatim. Purely additive; `reason` is unchanged.
|
|
1058
|
+
|
|
1059
|
+
**`@cogenta/api`:** `HealthDoctorCheck` gains an optional `reasonCode`
|
|
1060
|
+
field, carrying the same information through `GET /api/health-report`.
|
|
1061
|
+
|
|
1062
|
+
**`@cogenta/cli`:** `cogenta serve` now actually constructs a
|
|
1063
|
+
`ScheduledTaskRegistry` and mounts `createScheduledTasksRouter` under
|
|
1064
|
+
`/api/scheduled-tasks` — before this, the admin's "Tâches planifiées" screen
|
|
1065
|
+
had real, tested client and server code on both ends, but nothing ever
|
|
1066
|
+
wired them together, so every request 404'd through the generic content
|
|
1067
|
+
router. The seven recurring jobs that used to run on independent
|
|
1068
|
+
`setInterval`s (scheduled publication, the tools-queue drain, the 404 log
|
|
1069
|
+
purge, audit integrity, the trash sweep, forms GDPR retention, channel
|
|
1070
|
+
notification flush, analytics retention) now run through one heartbeat
|
|
1071
|
+
driving `registry.tick()`, at the same per-task cadence as before — "run
|
|
1072
|
+
now" from the admin is real, and last-run/next-run/history reflect the
|
|
1073
|
+
actual thing. `RuntimeExtras` gains an optional `scheduledTasksRouter`.
|
|
1074
|
+
`DoctorCheck` gains `reasonCode` and a typed `skipped` shape, matching
|
|
1075
|
+
`@cogenta/core`.
|
|
1076
|
+
|
|
1077
|
+
No breaking changes. A caller that never touches the new fields is
|
|
1078
|
+
unaffected; a site with no `ScheduledTaskRegistry` constructed by hand
|
|
1079
|
+
(a test harness building a bare `Site`) simply never gets the route mounted,
|
|
1080
|
+
same degradation as `agentsRouter`.
|
|
1081
|
+
|
|
1082
|
+
Also fixed, admin-only (`@cogenta/admin`, private, no changeset): the
|
|
1083
|
+
"Vues par jour" analytics chart now draws one bar per calendar day of the
|
|
1084
|
+
selected period — zero-filled where the server sent nothing — instead of
|
|
1085
|
+
stretching a sparse response into a single filled rectangle; the "Interroger
|
|
1086
|
+
le site" assistant tab shows an honest state instead of rendering blank when
|
|
1087
|
+
a provider is configured but `assist.chat` specifically is disabled; and the
|
|
1088
|
+
Agents screen degrades to its already-honest "no agent running" empty state
|
|
1089
|
+
instead of showing the raw `"No route matches this path."` wire text when no
|
|
1090
|
+
`AgentRegistry` is mounted (still the case on every real `cogenta serve`
|
|
1091
|
+
today — that gap is documented, not new).
|
|
1092
|
+
- 3cbd6d7: L22 task 5 — OpenTelemetry request tracing, a configurable log level, and
|
|
1093
|
+
an admin "Exploitation" > Observability screen, all on by default and
|
|
1094
|
+
working with zero external service (R1).
|
|
1095
|
+
|
|
1096
|
+
**New package `@cogenta/observability`:** wraps `@opentelemetry/api` +
|
|
1097
|
+
`@opentelemetry/sdk-trace-base` (a real new dependency — see the task
|
|
1098
|
+
report for size and maintenance detail; this is the industry-standard
|
|
1099
|
+
choice, never a hand-rolled tracer). `createObservabilityRuntime` builds
|
|
1100
|
+
one server span per HTTP request (`withRequestTracing`) and a bounded,
|
|
1101
|
+
in-process "recent events" buffer (`ObservabilityRecentStore`, same ring-
|
|
1102
|
+
buffer shape `@cogenta/core`'s `createErrorLog` already uses) that the
|
|
1103
|
+
admin reads. A local NDJSON exporter runs always, needing no external
|
|
1104
|
+
service; an OTLP HTTP exporter runs in addition when an endpoint is
|
|
1105
|
+
configured — never one hardcoded vendor, any OTLP-speaking backend
|
|
1106
|
+
(Grafana, Datadog, …) works. `withRecentLogCapture` wraps any
|
|
1107
|
+
`@cogenta/core` `Logger` so its records also feed the same buffer, gated
|
|
1108
|
+
by a dynamic level getter rather than the logger's own fixed threshold.
|
|
1109
|
+
Every field passes through `@cogenta/core`'s `redact()` before storage —
|
|
1110
|
+
the same discipline the audit log already applies — and a trace only ever
|
|
1111
|
+
carries a request's method, path (query string stripped) and status code,
|
|
1112
|
+
never a header, cookie, or body.
|
|
1113
|
+
|
|
1114
|
+
**`@cogenta/core`:** a new `observability` config section
|
|
1115
|
+
(`cogenta.config.mjs`) — `serviceName` and `otlpEndpoint`, resolved
|
|
1116
|
+
always, defaults needing nothing external. No `otlpHeaders` field, on
|
|
1117
|
+
purpose (rule R7, same shape as `payment`'s missing `stripeSecretKey`):
|
|
1118
|
+
those come from `COGENTA_OTLP_HEADERS`/`OTEL_EXPORTER_OTLP_HEADERS` only,
|
|
1119
|
+
refused if written to the file (`CONFIG_SECRET_IN_FILE`). `serviceName`
|
|
1120
|
+
and `otlpEndpoint` also honour the standard `OTEL_SERVICE_NAME`/
|
|
1121
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` environment variables as a fallback.
|
|
1122
|
+
|
|
1123
|
+
**`@cogenta/schema`:** `SITE_SETTINGS_REGISTRY` gains a new `observability`
|
|
1124
|
+
group with two editorial settings — `observability.enabled` (default on)
|
|
1125
|
+
and `observability.logLevel` (`error`/`warn`/`info`/`debug`, default
|
|
1126
|
+
`info`) — changeable from the admin with no restart, unlike the OTLP
|
|
1127
|
+
export destination above.
|
|
1128
|
+
|
|
1129
|
+
**`@cogenta/api`:** `createObservabilityRouter` — `GET /api/observability`,
|
|
1130
|
+
admin-only, read-only, answering the current `enabled` state plus the
|
|
1131
|
+
recent traces and logs.
|
|
1132
|
+
|
|
1133
|
+
**`@cogenta/cli`:** `cogenta serve` wires all of the above — the HTTP
|
|
1134
|
+
listener is wrapped with `withRequestTracing`, the shared logger is
|
|
1135
|
+
wrapped with `withRecentLogCapture`, and `observability.enabled`/
|
|
1136
|
+
`observability.logLevel` are polled from the settings store every 15s
|
|
1137
|
+
(configurable via `ServeOptions.observabilitySettingsTickMs`, a test
|
|
1138
|
+
seam) so an admin's change takes effect without a restart.
|
|
1139
|
+
- 249eb6f: Add the update system (L22 task 9): checking npm for a newer `@cogenta/core`/
|
|
1140
|
+
`@cogenta/cli`, and applying one with a mandatory restore point first — never an
|
|
1141
|
+
update with no safety net.
|
|
1142
|
+
|
|
1143
|
+
`@cogenta/core` gains `readOwnPackageVersion` (self-describing package version,
|
|
1144
|
+
read from a package's own `package.json`, never bundled at build time) and
|
|
1145
|
+
`getCoreVersion`, its own version computed with it — **lazily, cached after the
|
|
1146
|
+
first real call, never a top-level constant**: a top-level `CORE_VERSION =
|
|
1147
|
+
readOwnPackageVersion(...)` was the first design, and it broke every
|
|
1148
|
+
`@cogenta/admin` test that happened to pull `@cogenta/core` in transitively,
|
|
1149
|
+
because that suite's `import.meta.url` is not a `file://` URL under
|
|
1150
|
+
Vitest+jsdom's Vite transform. `@cogenta/core` is imported (for types) by
|
|
1151
|
+
enough of this monorepo, including browser-bundled code, that nothing at its
|
|
1152
|
+
top level may assume a real Node `file://` module URL — fixed before it ever
|
|
1153
|
+
shipped, but worth naming so the next self-describing constant doesn't repeat
|
|
1154
|
+
it. New error codes: `PACKAGE_VERSION_UNREADABLE`, `UPDATE_CHECK_FAILED`,
|
|
1155
|
+
`UPDATE_RESTORE_POINT_FAILED`, `UPDATE_APPLY_FAILED`, `UPDATE_NOT_AVAILABLE`,
|
|
1156
|
+
`UPDATE_CONFIRMATION_REQUIRED`, `UPDATE_POLICY_INVALID`.
|
|
1157
|
+
|
|
1158
|
+
`@cogenta/schema` gains one new site-settings-registry entry,
|
|
1159
|
+
`updates.autoUpdatePolicy` (`off`/`patch`/`patch-minor`/`patch-minor-major`, off by
|
|
1160
|
+
default) — a normal editorial setting through the existing generic settings store,
|
|
1161
|
+
no new persistence mechanism.
|
|
1162
|
+
|
|
1163
|
+
`@cogenta/api` gains `createUpdateRouter`: `GET /api/updates/status` (a live
|
|
1164
|
+
version check against npm, per package), `GET /api/updates/history` (past
|
|
1165
|
+
checks/applies plus the restore points they took), and `POST /api/updates/apply`
|
|
1166
|
+
(admin-only, every route).
|
|
1167
|
+
|
|
1168
|
+
`@cogenta/cli` gains `cogenta update check|apply|history`, wired the same way into
|
|
1169
|
+
`cogenta serve`'s admin API and into a new daily `updates-auto-check` scheduled
|
|
1170
|
+
task that honours `updates.autoUpdatePolicy` — never auto-applies a version whose
|
|
1171
|
+
changelog scan flagged a frozen contract, and never re-applies the same version on
|
|
1172
|
+
every tick after a successful auto-apply (this process's own version constant
|
|
1173
|
+
cannot change without an actual restart).
|
|
1174
|
+
|
|
1175
|
+
A **real bug fix**, found while wiring `getCliVersion`: `bin.ts` never passed its
|
|
1176
|
+
own version to `run()`, so `cogenta version`/`cogenta --version` always printed the
|
|
1177
|
+
fallback `"0.0.0"` regardless of what was actually installed. Fixed.
|
|
1178
|
+
|
|
1179
|
+
**Contract-risk detection is real but honestly limited.** It reads the target
|
|
1180
|
+
version's own published `CHANGELOG.md`, fetched from its npm tarball
|
|
1181
|
+
(`registry.npmjs.org` only, a small zero-dependency ustar/pax reader — no `tar`
|
|
1182
|
+
dependency, R9) and scanned for a frozen-contract mention. `@cogenta/core` and
|
|
1183
|
+
`@cogenta/cli` add `CHANGELOG.md` to their own `"files"` for this to work — every
|
|
1184
|
+
version already published before this ships has no `CHANGELOG.md` in its tarball
|
|
1185
|
+
(verified with a real `npm pack` while building this), so the check reports an
|
|
1186
|
+
honest "could not determine" for those rather than a false "no risk found." Even
|
|
1187
|
+
once readable, this is a keyword scan of prose, not comprehension — a strong hint
|
|
1188
|
+
an admin reviews before confirming, never a certification.
|
|
1189
|
+
|
|
1190
|
+
**Out of scope, deliberately**: this updates a site's npm packages only — `cogenta
|
|
1191
|
+
build`/`deploy` remain honestly deferred (L9), and no migration ever runs
|
|
1192
|
+
automatically (`cogenta migrate status`/`migrate up` stay a separate, explicitly
|
|
1193
|
+
confirmed step, exactly as today).
|
|
1194
|
+
- 4d3f3c7: L24 task 1: the agent execution loop (`packages/agents/src/runtime/loop.ts`, `runAgentLoop`) now runs as a two-node LangGraph.js `StateGraph` (`agent` → `tools` → `agent`) instead of a hand-written `for` loop. This was requested directly by the project owner after an earlier refusal of LangGraph in L22 (R9 — the hand-rolled loop was under 300 lines and sufficient at the time); the owner re-requested it for long-term maturity and stability, tracked in ADR-0029 (text ready, awaiting human insertion into `docs/03-decisions.md`).
|
|
1195
|
+
|
|
1196
|
+
**New direct dependency**: `@langchain/langgraph` (`^1.4.12`). Pure ESM, TypeScript, no native code (R10 n/a — nothing to WASM-fallback). Pulls ~16 transitive packages, including `@langchain/core` (peer dependency, resolved automatically, not added as a direct dependency of `@cogenta/agents` since nothing here imports from it — only `StateGraph`, `Annotation`, `START`, `END`, `GraphRecursionError` are used, never LangChain's message types or its own tool-calling/agent abstractions) and, further down, `langsmith` — LangChain's proprietary tracing SDK. **`langsmith` is never called, configured, or reachable from any code in this repository**; it is a transitive pull with no on/off switch, not a forgotten integration. Flagged here explicitly so a future dependency audit does not mistake silence for an oversight.
|
|
1197
|
+
|
|
1198
|
+
**What did not change**: `runAgentLoop`'s public signature (`RunAgentLoopInput` in, `RunResult` out) is untouched, so every existing caller — `agents/orchestrator.ts` (`createAgentRunner`), `subagents/run-subagent.ts`, `tools/core/agent-delegate.ts`, `assist/runtime.ts`, `eval/run-suite.ts` — needed no changes at all. All three autonomy levels (`report-only`/`co-pilot`/`autopilot`) and the built-in agents behave identically from the admin's point of view. Contract C (`buildManifest`/`createToolRegistry`) does not change shape.
|
|
1199
|
+
|
|
1200
|
+
**R4, proven not assumed**: the graph's `tools` node contains zero permission logic — it calls a new exported primitive, `runTool`, which does nothing but look up a tool by name and call `.execute()`. The only thing standing between a model's tool-call request and a real side effect is whether the `ExecutableTool` object the node was handed was wrapped by `withAutonomy` before the graph ever saw it — a decision made entirely in `agents/orchestrator.ts`, outside and above the graph, exactly as before the migration. `packages/agents/test/runtime/loop.test.ts` adds three tests that prove this rather than assume it survived: `runTool` given a *raw* tool executes the real side effect (showing the node itself supplies no gate — if it did, this call would be blocked too); the same primitive given the *same* tool `withAutonomy`-wrapped at `observe` never reaches the side effect; and a full `runAgentLoop` run, with a model that asks for the same "dangerous" tool on three consecutive turns, never triggers it once.
|
|
1201
|
+
|
|
1202
|
+
`@cogenta/core` gains one new error code, `AGENT_LOOP_RECURSION_LIMIT` — a defensive backstop thrown only if LangGraph's own recursion ceiling were ever hit before `runAgentLoop`'s pre-existing `max_steps` guard fires first (the ceiling is set to `maxSteps * 2 + 10`, comfortably above what the guard needs, so this should be unreachable in practice; it exists to fail loudly rather than silently if that assumption is ever wrong).
|
|
1203
|
+
|
|
1204
|
+
`deps-auditor` was invoked on this addition before committing, per R9. Verdict: accept — MIT-licensed throughout, ESM, no native code, actively maintained (all four LangChain packages checked were last published within two days of this addition), and the alternative was already weighed and rejected once (L22, R9) before the owner explicitly re-requested it for long-term ecosystem maturity. One additional watch point beyond `langsmith`: the transitive tree carries two non-deduplicated versions of `p-queue` (6.6.2 via `langsmith`, 9.x via `@langchain/langgraph-sdk`) — no measured functional impact, worth revisiting only if `node_modules` size becomes a constraint on shared/mutualised hosting.
|
|
1205
|
+
- cb62917: `cogenta mcp --api-key <key>` — resolve the MCP server's actor from a real API
|
|
1206
|
+
key, and a dedicated admin screen to mint one (L21 task 6).
|
|
1207
|
+
|
|
1208
|
+
Until now, `cogenta mcp` could only run as a real user (`--email`), a
|
|
1209
|
+
synthetic test actor (`--role`), or anonymous — there was no way to generate
|
|
1210
|
+
a credential for an MCP client from the admin, the way REST already lets you
|
|
1211
|
+
via "Clés API". `--api-key` closes that gap by resolving through the exact
|
|
1212
|
+
same `ApiKeyStore` (`@cogenta/auth`) and "roles = scope" mapping REST's own
|
|
1213
|
+
`resolveActor` uses for a `cogenta_sk_…` bearer token — one store, two
|
|
1214
|
+
callers, never a second lookup path. A role the key was not granted is
|
|
1215
|
+
refused by the same `PermissionLayer` REST uses, exactly as it would be over
|
|
1216
|
+
HTTP (R4). `@cogenta/core` gains `MCP_ACTOR_API_KEY_INVALID` for an unknown,
|
|
1217
|
+
revoked or expired key.
|
|
1218
|
+
|
|
1219
|
+
The admin gains a new **MCP** screen (`@cogenta/admin`, private, no
|
|
1220
|
+
changeset entry of its own), parallel to "Agents" rather than folded into
|
|
1221
|
+
the generic "Clés API" screen — same underlying key store, different
|
|
1222
|
+
audience: generating a key here also shows a ready-to-paste `cogenta mcp
|
|
1223
|
+
--api-key …` command and a standard MCP client JSON configuration block,
|
|
1224
|
+
both built from the raw key the server just returned, shown exactly once,
|
|
1225
|
+
same as the existing screen's own raw-key handling.
|
|
1226
|
+
|
|
1227
|
+
See `packages/mcp/README.md` for the updated connection instructions.
|
|
1228
|
+
- 5e43b20: `cogenta mcp` — a real MCP (Model Context Protocol) server, wired in (L20 audit).
|
|
1229
|
+
|
|
1230
|
+
`@cogenta/mcp`'s server/transport existed, tested and unused since it shipped:
|
|
1231
|
+
no CLI command ever invoked it. `cogenta mcp` starts it for real, on the
|
|
1232
|
+
process's own stdin/stdout, built from this site's actual tool manifest
|
|
1233
|
+
(`buildManifest`, `@cogenta/agents`) — the same shape `cogenta serve` builds
|
|
1234
|
+
for REST/GraphQL, not a second implementation.
|
|
1235
|
+
|
|
1236
|
+
**R4 applied for real**: `--email <email>` resolves the acting user from the
|
|
1237
|
+
site's own user store and runs every tool call with that user's real roles,
|
|
1238
|
+
checked by the same `PermissionLayer` REST and GraphQL use. `--role
|
|
1239
|
+
<role,role>` hands a synthetic actor for local testing. With neither, calls
|
|
1240
|
+
run as the anonymous `public` actor — content tools stay on the manifest and
|
|
1241
|
+
stay permission-checked (a public actor sees only what a public actor may
|
|
1242
|
+
see); media, site-config and HTTP-fetch tools, which have no permission
|
|
1243
|
+
check of their own, are left off the manifest entirely rather than exposed
|
|
1244
|
+
by default.
|
|
1245
|
+
|
|
1246
|
+
`@cogenta/core` gains three error codes: `MCP_ACTOR_OPTIONS_CONFLICT`,
|
|
1247
|
+
`MCP_ACTOR_USER_NOT_FOUND`, `MCP_ACTOR_ROLE_EMPTY`.
|
|
1248
|
+
|
|
1249
|
+
See `packages/mcp/README.md` for how to connect Claude Desktop, Claude Code
|
|
1250
|
+
or Cursor, and `BLOCKERS.md` §18 for the one known limitation: content
|
|
1251
|
+
written through this path is not (yet) re-indexed for search/vectors or
|
|
1252
|
+
redirect-tracked the way `cogenta serve`'s own write path is.
|
|
1253
|
+
- b8d307a: Fiche 58: the "MCP" admin screen renamed to "MCP Server" (nav/i18n only, no
|
|
1254
|
+
functional change — task 1), and a real MCP **client**: this site's own agents can
|
|
1255
|
+
now consume external MCP servers, gated by a security review (`security-reviewer`,
|
|
1256
|
+
2026-08-26 — NO-GO as originally written, GO conditional on a sandboxing floor,
|
|
1257
|
+
re-reviewed against this final implementation before merge).
|
|
1258
|
+
|
|
1259
|
+
**`@cogenta/mcp`**: `createMcpStdioClient` no longer inherits `process.env` —
|
|
1260
|
+
`spawn` receives exactly `options.env ?? {}`, never the host's real environment
|
|
1261
|
+
(the critical finding: the previous default handed a spawned third-party process
|
|
1262
|
+
every secret this server had, `COGENTA_AUTH_SIGNING_KEY` included, before
|
|
1263
|
+
`initialize()` was ever called). `stdio` is always `['pipe', 'pipe', 'pipe']`,
|
|
1264
|
+
never `inherit` — stderr is captured and logged through the structured logger,
|
|
1265
|
+
capped in size. Every JSON-RPC call has a hard timeout that kills the process and
|
|
1266
|
+
rejects every pending call on the connection; `wrapMcpTool`'s `execute` now honours
|
|
1267
|
+
`ctx.signal` too, so a run's own cancellation reaches the remote process the same
|
|
1268
|
+
way. A best-effort memory/CPU watchdog polls the spawned PID (`ps`/PowerShell, no
|
|
1269
|
+
native dependency — R9/R10); the real limit is host-level (cgroup, Job Object),
|
|
1270
|
+
documented as a prerequisite, not a guarantee.
|
|
1271
|
+
|
|
1272
|
+
New `packages/mcp/src/registry/`: `McpConnectionStore` (table `mcp_connections`,
|
|
1273
|
+
secret encrypted at rest with the same AES-256-GCM/`COGENTA_AUTH_SIGNING_KEY`
|
|
1274
|
+
scheme as `@cogenta/agents`' `ProviderConfigStore` — R7), `discoverMcpConnection`
|
|
1275
|
+
(a real `initialize()` + `tools/list()` probe through the sandboxed client),
|
|
1276
|
+
`buildMcpToolDefinitions` (wires every enabled connection's checked tools into
|
|
1277
|
+
Contract C `ToolDefinition`s). `McpConnectionStore.create()` structurally refuses a
|
|
1278
|
+
`stdio` connection without `confirmUnsandboxed: true` — the mandatory, honest
|
|
1279
|
+
acknowledgement that this binary runs with the Cogenta process's own full OS
|
|
1280
|
+
privileges, unsandboxed beyond this package's floor; a UI can show the warning, but
|
|
1281
|
+
the refusal itself lives here. `setExposedTools()` refuses a remote tool name never
|
|
1282
|
+
actually seen in the connection's last discovered list — "absent, pas refusée": a
|
|
1283
|
+
tool the admin never checked is never wrapped for any agent. `http` is a stored
|
|
1284
|
+
transport (forward-compatible schema) with no working client yet — honestly
|
|
1285
|
+
refused (`discoverMcpConnection`), never silently pretended to work.
|
|
1286
|
+
|
|
1287
|
+
**Contract C → `tools@1.4`** (`docs/04-contrats.md`): the parameterised permission
|
|
1288
|
+
`mcp.external:<connectionId>.<remoteToolName>` — one permission per checked remote
|
|
1289
|
+
tool, never per connection (`mcp.external.<connexion>` was rejected by the security
|
|
1290
|
+
review: it would grant every checked tool on a connection indifferently of its own
|
|
1291
|
+
risk, contradicting the "case à cocher par outil" principle and weakening R4). No
|
|
1292
|
+
existing tool signature changes — additive to an open taxonomy, the same kind of
|
|
1293
|
+
change `document.extract`/`logs.read`/`redirects.write`/`code.patch` already were.
|
|
1294
|
+
|
|
1295
|
+
**`@cogenta/core`**: ten new error codes — `MCP_CLIENT_CALL_TIMEOUT`,
|
|
1296
|
+
`MCP_CLIENT_CALL_ABORTED`, `MCP_CLIENT_PROCESS_EXITED`, `MCP_CLIENT_SPAWN_FAILED`,
|
|
1297
|
+
`MCP_CLIENT_CLOSED`, `MCP_CLIENT_RESOURCE_EXCEEDED`, `MCP_CONNECTION_NOT_FOUND`,
|
|
1298
|
+
`MCP_CONNECTION_INVALID`, `MCP_CONNECTION_AUTH_INVALID`,
|
|
1299
|
+
`MCP_CONNECTION_CONFIRMATION_REQUIRED`, `MCP_CONNECTION_TOOL_NOT_DISCOVERED`.
|
|
1300
|
+
|
|
1301
|
+
**`@cogenta/api`**: new `createMcpConnectionsRouter` (`/api/mcp-connections`,
|
|
1302
|
+
admin-only) — list/create/enable-disable/remove, `POST .../test` (a real discovery
|
|
1303
|
+
probe), `PUT .../exposed-tools` (the admin's checkbox decision). A new direct
|
|
1304
|
+
dependency on `@cogenta/mcp` (internal workspace package, not a third-party
|
|
1305
|
+
addition) for `discoverMcpConnection` and the store's types.
|
|
1306
|
+
|
|
1307
|
+
**`@cogenta/cli`**: `cogenta serve` creates the connection table and store
|
|
1308
|
+
unconditionally (usable even without an LLM provider configured, same posture as
|
|
1309
|
+
`/api/api-keys`); `packages/cli/src/commands/agent-runtime.ts`'s `buildAgentRuntime`
|
|
1310
|
+
merges every enabled connection's checked tools into the site's real tool registry
|
|
1311
|
+
through a live-swappable wrapper (`createLiveToolRegistry`) — a connection
|
|
1312
|
+
created/tested/exposed from the admin screen becomes callable by an agent on its
|
|
1313
|
+
very next lookup, no `cogenta serve` restart, the same "no restart needed"
|
|
1314
|
+
guarantee `/api/providers` already gives. `AgentRuntimeAssembly` gains
|
|
1315
|
+
`refreshMcpTools()` and `mcpDispose()` (closes every spawned `McpClient` and
|
|
1316
|
+
removes every sandbox working directory on server shutdown). The fiche names
|
|
1317
|
+
`packages/agents/src/runtime/` for this wiring; it lives in `@cogenta/mcp`/
|
|
1318
|
+
`@cogenta/cli` instead — `@cogenta/mcp` already depends on `@cogenta/agents`, so
|
|
1319
|
+
the reverse dependency the fiche's own path would need is a package cycle. Deviation
|
|
1320
|
+
signalled, not silently worked around.
|
|
1321
|
+
|
|
1322
|
+
Tests: `@cogenta/mcp` — the sandboxing floor (no inherited environment variable
|
|
1323
|
+
proven by inspecting what `spawn` actually receives while a real host secret is
|
|
1324
|
+
set; a hung server killed and rejected under a configured timeout; per-call abort;
|
|
1325
|
+
stderr capture), the connection store (confirmation requirement, encrypted secret,
|
|
1326
|
+
"absent, pas refusée"), discovery, and `buildMcpToolDefinitions` (one client shared
|
|
1327
|
+
across a connection's tools, a failed connection skipped not thrown, an end-to-end
|
|
1328
|
+
call through a fake stdio server). `@cogenta/api` — admin-only, the confirmation
|
|
1329
|
+
refusal, "absent, pas refusée" at the REST boundary. `@cogenta/cli` — a real,
|
|
1330
|
+
spawned `node` process (`test/fixtures/fake-mcp-server.mjs`) driven end to end
|
|
1331
|
+
through a real `cogenta serve`/SQLite/HTTP stack: connection created, tested,
|
|
1332
|
+
exposed, called by a real agent run with a scripted LLM vendor, proving the actual
|
|
1333
|
+
child process received none of the host's real environment
|
|
1334
|
+
(`COGENTA_AUTH_SIGNING_KEY` included) and that disabling a connection removes its
|
|
1335
|
+
tool from what an agent can call without a restart.
|
|
1336
|
+
- 54409f3: Media library (fiche 11): tags, usage tracking, in-place replace, and richer
|
|
1337
|
+
listing.
|
|
1338
|
+
|
|
1339
|
+
**Breaking for a custom `MediaStore` implementation**, written as `minor`
|
|
1340
|
+
following this project's established pre-alpha convention (0.x, no package
|
|
1341
|
+
has ever used `major`, and one here would jump straight to `1.0.0` — which
|
|
1342
|
+
"pre-alpha" contradicts). `@cogenta/core`'s `MediaStore` interface gains two
|
|
1343
|
+
new required methods, `count()` (the total match count ignoring
|
|
1344
|
+
`limit`/`cursor`, so the admin can show "2,000 assets" instead of only "there
|
|
1345
|
+
is another page") and `replace()` (overwrite the bytes behind an existing id
|
|
1346
|
+
in place — every entry and block already holding that id keeps working,
|
|
1347
|
+
unchanged). `MediaAsset` gains two new required fields: `tags` (free-form
|
|
1348
|
+
labels, not a hierarchy — an asset commonly belongs to more than one subject
|
|
1349
|
+
at once) and `contentHash` (a short digest of the stored bytes, folded into
|
|
1350
|
+
`/_image` URLs as `&v=` to bust the year-long immutable cache when an asset
|
|
1351
|
+
is replaced — never a secret, never used for integrity). The only
|
|
1352
|
+
implementation in this repo, `createDatabaseMediaStore`, is updated; a
|
|
1353
|
+
third-party driver is not.
|
|
1354
|
+
|
|
1355
|
+
Backward-compatible additions: `CreateMediaInput`/`UpdateMediaInput` gain
|
|
1356
|
+
optional `tags`; `ListMediaOptions` gains `tag`, `from`/`to` (created-at
|
|
1357
|
+
range), `sort` (`MediaSortField`: `createdAt`/`filename`/`size`), and
|
|
1358
|
+
`direction`. `@cogenta/render`'s `MediaAsset` gains an optional `version`
|
|
1359
|
+
field (`theme@1.2`) — absent is fully backward compatible, exactly today's
|
|
1360
|
+
behaviour with no `&v=` appended.
|
|
1361
|
+
|
|
1362
|
+
`@cogenta/api`'s `createMediaRouter` gains real multipart parsing
|
|
1363
|
+
(`packages/api/src/rest/multipart.ts`, zero new dependency — R9/R10), a
|
|
1364
|
+
`POST /api/media/{id}/replace` route, `tag`/`from`/`to`/`sort`/`direction`
|
|
1365
|
+
query parameters on the list route, and EXIF GPS stripping on upload and
|
|
1366
|
+
replace (`stripGps`, opt-out per request, default on — a photo's location is
|
|
1367
|
+
not something an editor usually means to publish).
|
|
1368
|
+
|
|
1369
|
+
`@cogenta/schema` gains `findMediaUsage` (`packages/schema/src/media-usage.ts`):
|
|
1370
|
+
scans every collection's entries for a media id in a `media`/`richText`/
|
|
1371
|
+
`blocks` field and reports where it is referenced, so the admin can warn
|
|
1372
|
+
before deleting an asset still in use rather than after. `titleOf` (from
|
|
1373
|
+
`search/extract.ts`) is now exported — `findMediaUsage` needed the same
|
|
1374
|
+
"what does an editor call this entry" logic the search indexer already had,
|
|
1375
|
+
and duplicating it would have drifted.
|
|
1376
|
+
- 2285720: Menus gain a real editor (fiche `docs/plans/09-menus.md`):
|
|
1377
|
+
|
|
1378
|
+
- **Edit an item in place.** `PATCH /api/menus/{id}/items/{itemId}` now accepts `label`, `kind`, the target fields, `title` and `openInNewTab` — no more delete-and-recreate to fix a typo. Changing `kind` clears the previous target rather than keeping a value that no longer applies. `parent` is deliberately not accepted here; re-parenting still goes through `POST .../move`.
|
|
1379
|
+
- **Bulk, transactional reorder.** `MenuStore.reorderItems` and `PATCH /api/menus/{id}/items` rewrite `parent`/`position` for any number of items in a single transaction, so a drag-and-drop or keyboard reordering session commits (or fails) as one unit — never a partially-rewritten tree if the network drops mid-session.
|
|
1380
|
+
- **Menu locations.** `Menu` gains `location: string | null` (`byLocation`, `GET /api/menus/by-location/{location}`) — where a menu renders (`primary`, `footer`, …), carried by the menu itself rather than baked into a theme's name convention. `@cogenta/cli`'s `ThemeRenderOptions` gains `headerMenuLocation`/`footerMenuLocation`, resolved generically by location with a fallback to the legacy `main`/`footer` name lookup, so an existing site's navigation keeps rendering unchanged. `@cogenta/core` gains the `MENU_LOCATION_TAKEN` error code for the one-menu-per-location-per-locale rule.
|
|
1381
|
+
- **Two new item kinds.** `taxonomy` (links to a term) and `home` (always resolves to `/`) join `entry`/`url`/`submenu-placeholder`.
|
|
1382
|
+
- **Target health.** A menu item resolver may now report `health` (`published`/`draft`/`scheduled`/`archived`/`trashed`) for an `entry` item — computed only for an actor whose role already has draft access to the target collection, so a public read never learns that a draft exists. `cogenta serve`'s public render hides a dead `entry`/`taxonomy`/`home` link entirely rather than serving one.
|
|
1383
|
+
|
|
1384
|
+
All additions are backward compatible: `resolveEntry` gained a third `context` parameter and an optional `health` on its result, but a two-argument resolver still satisfies the type; every new field is optional or nullable on the wire.
|
|
1385
|
+
- 9b1dae8: Fiche 43 sub-chantiers A, B, E, F (Cogenta Page Builder — motifs, copier/coller, verrouillage/sélection multiple, import/export) — extends the L16 visual page builder without touching contract A, B, C or D.
|
|
1386
|
+
|
|
1387
|
+
**Sub-chantier A — pattern/model library.** `@cogenta/schema` gains a new
|
|
1388
|
+
one-fixed-table store (`ensurePatternTables`/`createPatternStore`,
|
|
1389
|
+
`cogenta_patterns`), the same "not schema-declared, one fixed pair/table"
|
|
1390
|
+
treatment `menu-tables.ts` already gets — a pattern is a reusable *shape* an
|
|
1391
|
+
editor composes from existing blocks, never a thirteenth block type. Two
|
|
1392
|
+
kinds share the table: a **motif** (a few blocks, added to whatever a page
|
|
1393
|
+
already has) and a **modèle de page complet** (replaces the whole block
|
|
1394
|
+
zone, and only ever behind explicit confirmation in the admin — never
|
|
1395
|
+
silently). `@cogenta/api` gains `createPatternRouter` (`/api/patterns`,
|
|
1396
|
+
admin/editor only on every method, mirroring `redirect-router.ts`'s fixed
|
|
1397
|
+
door) with two new error codes on `@cogenta/core`, `PATTERN_UNKNOWN`/`PATTERN_INVALID`. A
|
|
1398
|
+
pattern's blocks are validated against the site's block registry
|
|
1399
|
+
(`@cogenta/blocks`'s `vocabularyRegistry` by default, overridable) exactly
|
|
1400
|
+
the way a clipboard paste is: one unknown block type refuses the whole
|
|
1401
|
+
pattern, never a partial or best-effort insert. `@cogenta/cli` wires both
|
|
1402
|
+
into `cogenta serve` (`ensurePatternTables` at boot, `/api/patterns` mounted
|
|
1403
|
+
next to `/api/menus`) and into `cogenta backup`/`cogenta restore`
|
|
1404
|
+
(`PATTERN_TABLE` added to the table list `buildBackupTables` already
|
|
1405
|
+
assembles).
|
|
1406
|
+
|
|
1407
|
+
**Sub-chantier B — copy/paste and reusable blocks.** Purely client-side
|
|
1408
|
+
(`@cogenta/admin`, no published package touched): `Ctrl/⌘+C`/`Ctrl/⌘+V` on
|
|
1409
|
+
the builder's block selection, through the browser clipboard as
|
|
1410
|
+
`cogenta/blocks@1`-tagged JSON, validated the same way on paste (unknown
|
|
1411
|
+
block type named and refused). "Blocs réutilisables" is deliberately not a
|
|
1412
|
+
second mechanism — fiche 05 task 3's own recommendation — a single-block
|
|
1413
|
+
pattern already covers it: insertion is always a copy, never a live
|
|
1414
|
+
reference, so there is nothing in contract B to touch.
|
|
1415
|
+
|
|
1416
|
+
**Sub-chantier E — lock and multi-select.** Also admin-only. A lock is a
|
|
1417
|
+
session-only admin flag, never persisted to contract B or the server; a
|
|
1418
|
+
locked block cannot be moved (by its own controls, by a neighbour's move
|
|
1419
|
+
displacing it, or as part of a group move) or removed. Multi-select is
|
|
1420
|
+
scoped to the outline list (`Shift`+click), never the preview — the same
|
|
1421
|
+
`Shift`+click a keyboard/switch user can also drive, with named group
|
|
1422
|
+
buttons doubling every drag, per the lot's own rule. A group move/remove is
|
|
1423
|
+
always one undo step, never one per block.
|
|
1424
|
+
|
|
1425
|
+
**Sub-chantier F — import/export.** A pattern library round-trips through a
|
|
1426
|
+
versioned JSON file (`cogenta/pattern-file@1`), validated block-by-block on
|
|
1427
|
+
import the same way a save is. `provenance`/`provenanceDetail` follow
|
|
1428
|
+
contract A's own values (`human`/`assisted`/`generated`) — a pattern an
|
|
1429
|
+
agent generates is never indistinguishable from one a person authored by
|
|
1430
|
+
hand.
|
|
1431
|
+
|
|
1432
|
+
`cogenta_patterns` has the same one-suite-run-four-times contract test as
|
|
1433
|
+
`taxonomy-store.ts`/`content-store.ts` (`pattern-store.contract.ts`,
|
|
1434
|
+
SQLite as a unit test and Postgres/MySQL/MariaDB as loud-skip integration
|
|
1435
|
+
tests) — deliberately not left SQLite-only the way `menu-store.ts`'s own
|
|
1436
|
+
table predates this discipline and still is.
|
|
1437
|
+
|
|
1438
|
+
No contract touched: A, B, C and D are all unchanged. `PermissionLayer`
|
|
1439
|
+
gains no new method — pattern management is a fixed admin/editor rule, the
|
|
1440
|
+
same shape `redirectRouter`/`menuRouter` already use, and *inserting* a
|
|
1441
|
+
pattern's blocks into an entry still goes through the entry's own existing
|
|
1442
|
+
`update` permission (`POST /api/builder/render`'s `PermissionLayer.assert`),
|
|
1443
|
+
unchanged.
|
|
1444
|
+
- 8a8d873: Add PayPal as a third, independently registered payment driver — proof that the payment
|
|
1445
|
+
gateway a shop uses is not a fixed Stripe/bank-transfer pair but an open registry
|
|
1446
|
+
(`@cogenta/commerce`'s `PaymentGateway` interface, the same `Driver<T, Config>` shape as
|
|
1447
|
+
cache/queue/storage), the concrete answer to "what if I don't want Stripe?".
|
|
1448
|
+
|
|
1449
|
+
- `@cogenta/commerce` gains `payment/paypal.ts` (`paypalPaymentDriver`), written against
|
|
1450
|
+
PayPal's REST Orders v2 / Payments v2 API with `fetch`, no new dependency (R9), the same
|
|
1451
|
+
discipline as `payment/stripe.ts`: OAuth2 client-credentials token caching, a real
|
|
1452
|
+
RSA-SHA256 webhook signature check against a certificate fetched from
|
|
1453
|
+
`paypal-cert-url` (trusted only when its origin matches `apiBaseUrl` or is a genuine
|
|
1454
|
+
`*.paypal.com` host — a forged cert-url header cannot "verify" against its own key), a
|
|
1455
|
+
freshness window, and an explicit event whitelist so an unrecognised PayPal event is
|
|
1456
|
+
refused rather than guessed as `paid`. `fetch()` captures an order the moment it sees
|
|
1457
|
+
`APPROVED` (there is no separate capture verb in this project's narrow
|
|
1458
|
+
`PaymentGateway` interface), tolerating the one real race a concurrent poll can hit
|
|
1459
|
+
(`ORDER_ALREADY_CAPTURED`) by re-reading the order instead of failing. Registered in
|
|
1460
|
+
`payment/registry.ts` alongside Stripe (both `optimal`) ahead of the always-available
|
|
1461
|
+
`manual` driver (`degraded`).
|
|
1462
|
+
- `@cogenta/core`'s `payment` configuration section gains `paypal` as a named driver and
|
|
1463
|
+
three secret fields (`paypalClientId`, `paypalClientSecret`, `paypalWebhookId`), refused
|
|
1464
|
+
in `cogenta.config.mjs` the same way Stripe's are and sourced only from
|
|
1465
|
+
`COGENTA_PAYMENT_PAYPAL_CLIENT_ID` / `COGENTA_PAYMENT_PAYPAL_CLIENT_SECRET` /
|
|
1466
|
+
`COGENTA_PAYMENT_PAYPAL_WEBHOOK_ID`.
|
|
1467
|
+
- `@cogenta/cli`'s `cogenta serve` passes the three PayPal fields through to
|
|
1468
|
+
`createPaymentRegistry` alongside the existing Stripe ones — no other wiring changed.
|
|
1469
|
+
- `@cogenta/admin` (private, no changeset): the payment screen is rebuilt from a
|
|
1470
|
+
two-card grid into a real provider list (WooCommerce's "Payment providers" pattern) —
|
|
1471
|
+
each row shows tier, configured/not-configured, active, and its own test-connection
|
|
1472
|
+
button — so a third driver appears with no change to the component, proving the point
|
|
1473
|
+
visually rather than only in code.
|
|
1474
|
+
|
|
1475
|
+
PayPal's sandbox and live environments are different hostnames (`api-m.sandbox.paypal.com`
|
|
1476
|
+
vs `api-m.paypal.com`), unlike Stripe's single host with a test/live key prefix — an
|
|
1477
|
+
operator testing against the sandbox sets `payment.apiBaseUrl` explicitly, the same escape
|
|
1478
|
+
hatch the driver's own test suite uses to point at a local HTTP stub.
|
|
1479
|
+
- 3075941: Fiche 45 — Prompt Settings, a shared, editable library for every utility prompt an `assist.*` tool sends the model. Until now, each instruction line (`assist.rewrite`, `assist.proofread`, `assist.summarise`, `assist.translate`, `assist.meta_description`, `assist.titles`, `assist.tags`, `assist.alt_text`, `assist.classify`, `assist.moderate`, `assist.faq_draft`, `assist.schema_org_draft`, `assist.chat`) was a literal string baked into the package.
|
|
1480
|
+
|
|
1481
|
+
`@cogenta/agents` gains a new `prompts/` module: `PromptTemplateStore` (`createFilePromptTemplateStore` — one JSON file per template, same "real but local" tier as the existing agent/skill/provider stores, R1), `renderPromptTemplate`/`resolveInstruction` (`{{field}}` placeholder substitution that throws `PROMPT_TEMPLATE_PLACEHOLDER_UNRESOLVED` rather than sending a literal unresolved placeholder to the model), and `builtinPromptTemplateSeeds`/`ensureBuiltinPromptTemplates` (thirteen templates reproducing every existing `assist.*` instruction verbatim as editable text, plus two new ones — `generate_text_block` for the future page-builder "Générer" button and `generate_agent_system_prompt` for the future agent-creation flow — written with the same care as a built-in agent's `identity.md`).
|
|
1482
|
+
|
|
1483
|
+
Every migrated `assist.*` tool constructor now accepts an optional trailing `PromptTemplateStore` argument (`createWritingTools`, `createClassifyTool`, `createModerateTool`, `createFaqTool`, `createSchemaOrgTool`, `createContentChatTool`'s options). Backward compatible: omitting it (or a site whose store has never been seeded) reproduces the exact pre-existing hard-coded instruction, byte for byte — proven by a dedicated non-regression test comparing the seeded-store path against the original inline construction for every migrated tool. A tool's `role`/objectives and the R8 anti-injection rule stay in code, deliberately not migrated — they are the security boundary, not the prompt text an editor should be able to reword from a settings screen.
|
|
1484
|
+
|
|
1485
|
+
`@cogenta/api` gains `createPromptTemplatesRouter` (`/api/prompt-templates`) — `GET` open to any signed-in actor, `POST`/`PATCH`/`DELETE` restricted to `admin`, mirroring `agent-skills-router.ts`'s shape. New `ErrorCode`s (`@cogenta/core`): `PROMPT_TEMPLATE_UNKNOWN` (404), `PROMPT_TEMPLATE_DUPLICATE` (409), `PROMPT_TEMPLATE_BUILTIN_UNDELETABLE` (409), `PROMPT_TEMPLATE_INVALID` (400), `PROMPT_TEMPLATE_PLACEHOLDER_UNRESOLVED` (400).
|
|
1486
|
+
|
|
1487
|
+
`@cogenta/cli`'s `cogenta serve` now builds a `PromptTemplateStore` under `.cogenta/agents-runtime/prompt-templates` (seeded on first boot, idempotent) and threads it through both `buildAssistant` (so the writing-assistant tools resolve their instruction text from it) and `buildAgentRuntime` (which mounts `/api/prompt-templates`) — the same directory, two file-store instances, safe because neither caches across calls.
|
|
1488
|
+
|
|
1489
|
+
The admin's "Prompt Settings" screen (`packages/admin`, private, no changeset) is a new admin-only entry in the AI nav group: list/create/edit/delete a template, with a builtin always editable but never removable.
|
|
1490
|
+
- e01efae: T09-04 (RGPD, audit 2026-09-01) — `exportPersonalData` (`@cogenta/export`) had zero
|
|
1491
|
+
callers anywhere in the codebase; the legal obligation it exists to satisfy was not
|
|
1492
|
+
exerciseable. `@cogenta/api`'s `users-router.ts` gains `GET /api/users/{id}/personal-data`
|
|
1493
|
+
(self-or-admin, the same rule `GET /{id}` itself already follows) — assembles the
|
|
1494
|
+
account, every collection entry it authored (via the same `storeFor` REST/GraphQL/theme
|
|
1495
|
+
rendering already share, now a `UsersRouterOptions` field), and the honest `gaps` array
|
|
1496
|
+
`exportPersonalData` reports for domains this codebase has no store for yet. The export
|
|
1497
|
+
is itself journalled (`user.personal_data_export`, naming whether it was a self-request
|
|
1498
|
+
or an admin acting on a third party). `cogenta serve` wires `storeFor` into the router;
|
|
1499
|
+
the admin gains an "Export my personal data" button on the profile screen (every role,
|
|
1500
|
+
self only) and an "Export the personal data of {{email}}" action per account row on the
|
|
1501
|
+
Users screen (admin, any account). New direct dependency `@cogenta/api` → `@cogenta/export`
|
|
1502
|
+
(R9: reusing an existing, tested assembly function rather than a second one).
|
|
1503
|
+
|
|
1504
|
+
T09-01 — `AuditLog.prune()` (`@cogenta/auth`) has existed since fiche 21 task 5 with no
|
|
1505
|
+
scheduled caller, so an audit log grew without bound on every site regardless of
|
|
1506
|
+
retention intent. `@cogenta/core` gains `security.audit.retainDays` (optional; absent —
|
|
1507
|
+
the default — changes nothing, `0` is the explicit "never purge" opt-out). `cogenta
|
|
1508
|
+
serve` registers a new daily `audit-prune` scheduled task (`Site.tickAuditPrune`) that
|
|
1509
|
+
purges entries older than the configured window and journals the purge itself
|
|
1510
|
+
(`audit.prune`, naming `retainDays`/`cutoff`/`prunedCount`) — a no-op when unconfigured.
|
|
1511
|
+
|
|
1512
|
+
T09-02 — `errorResponse` (`@cogenta/api`) gains a generic `Retry-After` header for any
|
|
1513
|
+
`CogentaError` whose `details.retryAfterMs` names a concrete backoff (only the derived
|
|
1514
|
+
integer ever reaches the wire, never `details` itself). `AUTH_RATE_LIMITED` — thrown by
|
|
1515
|
+
login and forgot-password rate limiting — is the first beneficiary: a 429 that used to
|
|
1516
|
+
say "try again later" in prose now carries a real, pollable `Retry-After`.
|
|
1517
|
+
- 5de237f: Fiche 63 (ADR-0028) — a role's grant on a collection or taxonomy action can
|
|
1518
|
+
now be overridden in the database, applied on the very next request with no
|
|
1519
|
+
deploy cycle. `cogenta.schema.*`'s `permissions` block stays the source of
|
|
1520
|
+
truth for a site that never writes an override; the database is checked
|
|
1521
|
+
first and falls back to the file, never the other way around.
|
|
1522
|
+
|
|
1523
|
+
`@cogenta/core` gains three error codes: `ROLE_PERMISSION_TARGET_UNKNOWN`
|
|
1524
|
+
(404 — an override names a collection/taxonomy the site does not declare),
|
|
1525
|
+
`ROLE_PERMISSION_INVALID` (400 — a malformed override, including `own` on a
|
|
1526
|
+
taxonomy, which has no author) and `ROLE_PERMISSION_EXPORT_INVALID` (a
|
|
1527
|
+
malformed `cogenta roles export` file being read back).
|
|
1528
|
+
|
|
1529
|
+
`@cogenta/schema` gains `createRolePermissionStore` (validates every write
|
|
1530
|
+
by folding the candidate rule into the real `CollectionDefinition`/
|
|
1531
|
+
`TaxonomyDefinition` and reusing `defineCollection`/`defineTaxonomy`
|
|
1532
|
+
unmodified — no second validation logic), `createRolePermissionOverlay` (the
|
|
1533
|
+
synchronous, refreshable read-through cache `PermissionLayer` consults),
|
|
1534
|
+
`ensureRolePermissionTable`/`ROLE_PERMISSIONS_TABLE`, and
|
|
1535
|
+
`serialiseRolePermissionExport`/`parseRolePermissionExport` for freezing the
|
|
1536
|
+
table's state into a versioned JSON file. All additive; contract A
|
|
1537
|
+
(`CollectionDefinition`, `TaxonomyDefinition`, `CollectionPermissions`) is
|
|
1538
|
+
unchanged — the override table lives entirely outside the contract.
|
|
1539
|
+
|
|
1540
|
+
`@cogenta/api`'s `createPermissionLayer` gains an optional
|
|
1541
|
+
`rolePermissionOverrides` option (a `RolePermissionOverrides` from
|
|
1542
|
+
`@cogenta/schema`) — absent behaves byte-for-byte as before. A new router,
|
|
1543
|
+
`createRolePermissionRouter`, serves `GET`/`PUT /api/role-permissions` and
|
|
1544
|
+
`DELETE /api/role-permissions/{targetType}/{targetName}/{action}`,
|
|
1545
|
+
admin-only. `STATUS_BY_CODE` gains the two new HTTP-mapped error codes above.
|
|
1546
|
+
|
|
1547
|
+
`@cogenta/cli` wires the override store and overlay into `cogenta serve`
|
|
1548
|
+
(mounting `/api/role-permissions`, journaling every successful write to the
|
|
1549
|
+
audit log), `cogenta mcp` and `cogenta channels` (each builds its own
|
|
1550
|
+
`PermissionLayer`, so each needed the same wiring — otherwise a permission
|
|
1551
|
+
revoked in production would stay granted to those processes until restart).
|
|
1552
|
+
A new command, `cogenta roles export [--out <path>]`, freezes the table into
|
|
1553
|
+
a file a site can commit to git.
|
|
1554
|
+
- 2c1af5d: Fiche 28 (tâches planifiées): a real scheduled-task registry and its admin
|
|
1555
|
+
screen — task 1 (registry) and task 2 (screen) complete and tested; task 4's
|
|
1556
|
+
concurrency-safe scheduled publication verified. `cogenta serve`'s own
|
|
1557
|
+
wiring of the registry, and the standalone `cogenta cron` command (task 5,
|
|
1558
|
+
for hosts with no permanent process), are **not done** — see below.
|
|
1559
|
+
|
|
1560
|
+
- `@cogenta/schema`'s `ScheduledTaskRegistry` (`createScheduledTaskRegistry`):
|
|
1561
|
+
each task declares a name, description, interval and run function; the
|
|
1562
|
+
registry persists every run (`cogenta_scheduled_task_runs`) — last run,
|
|
1563
|
+
duration, outcome, error — so "did the trash sweep run last night" survives
|
|
1564
|
+
a restart rather than resetting with an in-memory timer. `overdue` is
|
|
1565
|
+
computed from that persisted timestamp (fiche 28's own named pitfall: a
|
|
1566
|
+
detector that lives in memory is blind exactly when a restart makes it
|
|
1567
|
+
matter).
|
|
1568
|
+
- `@cogenta/api`'s `createScheduledTasksRouter` (`GET /api/scheduled-tasks`,
|
|
1569
|
+
`GET .../{name}`, `POST .../{name}/run`, `GET .../queue`,
|
|
1570
|
+
`POST .../queue/{id}/retry`) — admin-only, thin read-through, "run now"
|
|
1571
|
+
never awaits its own audit write so a slow log never hangs the request.
|
|
1572
|
+
- `@cogenta/core`'s `QueueDriver` gains `list()`/`retry()` — the "file" section
|
|
1573
|
+
of the screen, and the way a failed maintenance job (fiche 24's queue) gets
|
|
1574
|
+
retried from the UI instead of a terminal.
|
|
1575
|
+
- `@cogenta/core`'s config gains `scheduler.mode` (`'internal'` |
|
|
1576
|
+
`'external-cron'`) and `backup.*` (interval/keep/dir) — resolved, defaulted,
|
|
1577
|
+
not yet consumed by `cogenta serve` (see below).
|
|
1578
|
+
- Admin: `/scheduled` (new nav entry, admin-only at the route level — R4, the
|
|
1579
|
+
nav link itself is not the gate) — task table with last run/duration/
|
|
1580
|
+
result/next run, an overdue badge, "run now" with a confirmation dialog for
|
|
1581
|
+
a `destructive` task (the trash sweep), a queue section with retry, and a
|
|
1582
|
+
pointer to the dashboard's own scheduled-content list rather than a second
|
|
1583
|
+
copy of it.
|
|
1584
|
+
|
|
1585
|
+
**Genuinely not done, not just deferred quietly**: `cogenta serve` still
|
|
1586
|
+
drives scheduled publication, the trash sweep, the 404-log purge and the
|
|
1587
|
+
audit-integrity check on their own separate `setInterval`s, exactly as
|
|
1588
|
+
before this fiche — none of them are registered with the new
|
|
1589
|
+
`ScheduledTaskRegistry`. The registry and the admin screen above are real
|
|
1590
|
+
and fully tested against a registry populated by hand in their own test
|
|
1591
|
+
suites, but on a running `cogenta serve` today `/scheduled` would show an
|
|
1592
|
+
empty task list, because nothing calls `registry.register()` there yet.
|
|
1593
|
+
Wiring that in, and the `cogenta cron` command (task 5 — the fiche's own
|
|
1594
|
+
§8 leaves "deliver now or later" as an open decision), is real remaining
|
|
1595
|
+
work, not a rename or a config flag. Flagged here rather than left to be
|
|
1596
|
+
discovered later.
|
|
1597
|
+
- 745ebd8: Editorial workflow and owner permission (`schema@2.1`, ADR-0027, fiche 37 + fiche 19
|
|
1598
|
+
task 5).
|
|
1599
|
+
|
|
1600
|
+
Strictly additive — a site that never declares `workflow: { enabled: true }` on a
|
|
1601
|
+
collection, and never uses the `{ roles, own }` permission form, behaves identically
|
|
1602
|
+
to before this release. Proved by a compatibility test: a client reading only
|
|
1603
|
+
`status` gets byte-identical values.
|
|
1604
|
+
|
|
1605
|
+
- `reviewState` (`none`/`pending`/`changes-requested`/`approved`) and
|
|
1606
|
+
`assignedReviewer` join the system fields, orthogonal to `status` — the same design
|
|
1607
|
+
ADR-0022 gave `deletedAt`. `approved` is not `published`: approving authorises,
|
|
1608
|
+
`publish` remains the action that makes an entry public.
|
|
1609
|
+
- A closed, server-side transition table (`submit`/`approve`/`requestChanges`), each
|
|
1610
|
+
gated by its own contract A action (`update` for submit, `publish` for the other
|
|
1611
|
+
two) — never duplicated by a client.
|
|
1612
|
+
- New `ContentStore` methods `submitForReview`/`approveReview`/`requestReviewChanges`/
|
|
1613
|
+
`assignReviewer`, and new REST routes `POST .../submit`, `.../approve`,
|
|
1614
|
+
`.../request-changes`, `.../assign-reviewer` — each its own path, never a second
|
|
1615
|
+
meaning for an existing verb (ADR-0022's own lesson for `purge`).
|
|
1616
|
+
- `CollectionPermissionRule` gains the object form `{ roles, own? }` alongside the
|
|
1617
|
+
plain role-name array, which stays valid. `own: true` scopes every listed role to
|
|
1618
|
+
entries the acting account created; `PermissionLayer.can()`/`.assert()` take an
|
|
1619
|
+
optional `ownerId` to check it.
|
|
1620
|
+
- Reversible, non-destructive migration (`schema21Migration`) adding `review_state`
|
|
1621
|
+
(`not null default 'none'`) and a nullable `assigned_reviewer` to every collection.
|
|
1622
|
+
- Admin: a review queue screen (three tabs — assigned to me / all pending / my
|
|
1623
|
+
submissions — aggregated server-side via a new `GET /api/review`), a pending-count
|
|
1624
|
+
nav badge, and an entry editor sidebar showing workflow state, assigned reviewer,
|
|
1625
|
+
and a contextual action button that replaces the absent Publish button with
|
|
1626
|
+
"Submit for review" for an actor without `publish`.
|
|
1627
|
+
|
|
1628
|
+
Postgres/MySQL/MariaDB integration test files are written
|
|
1629
|
+
(`packages/schema/test/integration/schema-2-1-migration.test.ts`) but not executed
|
|
1630
|
+
this session — Docker unavailable; they skip loudly, naming the missing variable.
|
|
1631
|
+
- 960757d: Fiche 70 (SEO platform parity — AIOSEO/The SEO Framework/MonsterInsights/Site
|
|
1632
|
+
Kit) — four tasks closing the gaps a real research pass found against those
|
|
1633
|
+
four tools, which the earlier SEO fiches (13, 50) never looked at.
|
|
1634
|
+
|
|
1635
|
+
**Task 1 — real-time content score.** `@cogenta/seo` gains `analyseContent`
|
|
1636
|
+
(`content-analysis.ts`): a pure, synchronous TruSEO-style scorer over
|
|
1637
|
+
contract A's rich text — keyword usage in title/description/first sentence,
|
|
1638
|
+
keyword density, sentence length, subheadings, content length. Returns a
|
|
1639
|
+
closed `'red' | 'orange' | 'green'` score, never a numeric percentage. A new
|
|
1640
|
+
conventional field, `seoFocusKeyword`, joins `seoTitle`/`seoDescription`/etc.
|
|
1641
|
+
(contract A untouched). The admin panel keeps its own mirrored copy of the
|
|
1642
|
+
algorithm rather than depending on `@cogenta/seo`/`@cogenta/schema` — the
|
|
1643
|
+
admin is a browser bundle and never takes that dependency.
|
|
1644
|
+
|
|
1645
|
+
**Task 2 — internal link assistant.** `@cogenta/seo` gains
|
|
1646
|
+
`analyseInternalLinks` (`link-assistant.ts`), reusing `@cogenta/schema`'s
|
|
1647
|
+
existing `extractLinks`: reports entries with no inbound link and, for
|
|
1648
|
+
entries sharing title words, up to five link candidates. `@cogenta/api`'s
|
|
1649
|
+
`createSeoRouter` gains `GET /api/seo/link-suggestions?collection=…`, gated
|
|
1650
|
+
by `update` on the named collection (never `admin`) so an editor can run it
|
|
1651
|
+
on whatever they may already write.
|
|
1652
|
+
|
|
1653
|
+
**Task 3 — SEO feature grid.** Four new `seo.*` boolean settings
|
|
1654
|
+
(`contentScoreEnabled`, `linkAssistantEnabled`, `searchVerificationEnabled`,
|
|
1655
|
+
`robotsCustomRulesEnabled`) in `@cogenta/schema`'s site settings registry,
|
|
1656
|
+
all defaulting to `true` so an upgrading site's behaviour is unchanged. The
|
|
1657
|
+
last two are gated centrally inside `@cogenta/cli`'s `readSeoRenderDefaults`,
|
|
1658
|
+
so every consumer (public `robots.txt`, verification meta tags, the
|
|
1659
|
+
diagnostics scan) honours the toggle with no per-call-site duplication.
|
|
1660
|
+
|
|
1661
|
+
**Task 4 — optional Google Search Console connector (ADR-0032).**
|
|
1662
|
+
`@cogenta/seo` gains `search-console.ts`: a fetch-only OAuth client (no
|
|
1663
|
+
`googleapis` SDK) for the authorization URL, token exchange/refresh, and one
|
|
1664
|
+
read-only `searchAnalytics.query` call — structurally incapable of writing
|
|
1665
|
+
anything on the Google side. `@cogenta/schema` gains
|
|
1666
|
+
`createSearchConsoleConnectionStore`: one site-wide connection row,
|
|
1667
|
+
AES-256-GCM at rest via `COGENTA_AUTH_SIGNING_KEY` (same discipline as the
|
|
1668
|
+
LLM provider store), full SQLite/Postgres/MySQL/MariaDB contract suite.
|
|
1669
|
+
`@cogenta/api` gains `createSearchConsoleRouter`
|
|
1670
|
+
(`/api/seo/search-console/*`): `status`/`authorize`/`metrics`/`disconnect`
|
|
1671
|
+
are admin-only; `callback` (Google's own browser redirect target) carries no
|
|
1672
|
+
bearer token by design, proven legitimate instead by an HMAC-signed,
|
|
1673
|
+
ten-minute `state` token keyed by `COGENTA_AUTH_SIGNING_KEY`. `@cogenta/core`
|
|
1674
|
+
gains the `searchConsole` config section (client id/secret, environment-only,
|
|
1675
|
+
refused in the config file like every other secret) and five new error codes
|
|
1676
|
+
(`SEARCH_CONSOLE_NOT_CONFIGURED`/`_NOT_CONNECTED`/`_STATE_INVALID`/
|
|
1677
|
+
`_TOKEN_EXCHANGE_FAILED`/`_QUERY_FAILED`). Absent without both
|
|
1678
|
+
`COGENTA_SEARCH_CONSOLE_CLIENT_ID`/`_CLIENT_SECRET` set — every other SEO
|
|
1679
|
+
feature, including tasks 1-3 above, works identically with or without it
|
|
1680
|
+
(R1/R2), which was the explicit condition the user set when accepting
|
|
1681
|
+
ADR-0032.
|
|
1682
|
+
- 07c0f0a: Fiche 57 (Compétences : dossiers de référence standard) — a skill's
|
|
1683
|
+
`references/`, `scripts/` and `assets/` sub-folders, the standard layout a
|
|
1684
|
+
real Claude Code/Anthropic skill uses, are now created automatically and
|
|
1685
|
+
manageable from the admin. No contract A/B/C/D touched; no ADR required
|
|
1686
|
+
(that would only apply to a future `skill.read_resource` tool, which this
|
|
1687
|
+
fiche deliberately does not add).
|
|
1688
|
+
|
|
1689
|
+
**`@cogenta/agents`**: `AgentSkillStore` gains `listResources`,
|
|
1690
|
+
`addResource` and `removeResource`, plus the exported `SKILL_RESOURCE_DIRS`
|
|
1691
|
+
constant and `SkillResource`/`SkillResourceDir` types.
|
|
1692
|
+
`createFileAgentSkillStore`'s `create()` now also creates the three standard
|
|
1693
|
+
sub-folders, empty, alongside `SKILL.md`/`.meta.json`. Writing or removing a
|
|
1694
|
+
path outside `references/`, `scripts/` or `assets/` — or one that tries to
|
|
1695
|
+
escape the skill's own directory — is refused
|
|
1696
|
+
(`AGENT_SKILL_RESOURCE_INVALID`); a skill created before this fiche, with no
|
|
1697
|
+
sub-folders on disk, lists an empty resource set rather than erroring.
|
|
1698
|
+
|
|
1699
|
+
**`@cogenta/core`**: two new error codes, `AGENT_SKILL_RESOURCE_INVALID` and
|
|
1700
|
+
`AGENT_SKILL_RESOURCE_UNKNOWN`.
|
|
1701
|
+
|
|
1702
|
+
**`@cogenta/api`**: `agent-skills-router.ts` gains `GET`/`POST
|
|
1703
|
+
/api/agent-skills/:id/resources` and `DELETE
|
|
1704
|
+
/api/agent-skills/:id/resources/<path>`, all admin-only like the rest of the
|
|
1705
|
+
router. An upload accepts either a real `multipart/form-data` body (`path`
|
|
1706
|
+
field, `file` part — no base64 inflation for a binary asset) or a JSON body
|
|
1707
|
+
`{ path, content }` with `content` as plain UTF-8 text.
|
|
1708
|
+
`AgentSkillRegistryLike` gains the three matching methods; any other
|
|
1709
|
+
implementer of this interface needs to add them.
|
|
1710
|
+
|
|
1711
|
+
**`@cogenta/cli`**: `agent-runtime.ts`'s `createSkillRegistryAdapter` wires
|
|
1712
|
+
the three new methods straight through to `AgentSkillStore` — no new CLI
|
|
1713
|
+
command or flag.
|
|
1714
|
+
|
|
1715
|
+
**Admin** (not published, `@cogenta/admin`): the Compétences screen's edit
|
|
1716
|
+
row gains a "Fichiers de référence" panel — three lists (Références,
|
|
1717
|
+
Scripts, Gabarits) with upload and remove, using `FormData` uploads directly
|
|
1718
|
+
rather than the `fileToBase64` path `media-client.ts` still uses, since a
|
|
1719
|
+
resource file (an asset image, in particular) should not pay a ~33% base64
|
|
1720
|
+
inflation when a real `multipart/form-data` transport is already wired on
|
|
1721
|
+
the server side.
|
|
1722
|
+
|
|
1723
|
+
Nothing here is loaded into an agent's context automatically — deliberately
|
|
1724
|
+
so, per the fiche's own warning against uncontrolled context growth (R7).
|
|
1725
|
+
|
|
1726
|
+
### Patch Changes
|
|
1727
|
+
|
|
1728
|
+
- 154a751: Fiche 22 tâche 8 (finitions d'admin) — several small, independently useful
|
|
1729
|
+
changes across the published packages:
|
|
1730
|
+
|
|
1731
|
+
`@cogenta/core`'s `package.json` now declares `"./package.json"` in its
|
|
1732
|
+
`exports` map, so a dependent (`@cogenta/cli`) can resolve its own real
|
|
1733
|
+
installed version through Node's standard ESM resolution instead of a
|
|
1734
|
+
hand-maintained copy. Purely additive; nothing else in the package changes.
|
|
1735
|
+
|
|
1736
|
+
`@cogenta/schema`'s `SITE_SETTINGS_REGISTRY` gains a `navigation` group and
|
|
1737
|
+
four new keys (`navigation.sectionOrder`, `navigation.hiddenSections`,
|
|
1738
|
+
`navigation.itemOrder`, `navigation.hiddenItems`) — site-wide admin sidebar
|
|
1739
|
+
reordering and hiding (e.g. "hide the Commerce section on a portfolio
|
|
1740
|
+
site"), stored the same comma-separated-list way `content.
|
|
1741
|
+
newEntryDefaultBlocks` already is. Additive to the registry; no existing key
|
|
1742
|
+
changes shape or default.
|
|
1743
|
+
|
|
1744
|
+
`@cogenta/api`'s `ShellStatus` (and `createShellStatusRouter`'s
|
|
1745
|
+
`ShellStatusRouterOptions`) gains `cogentaVersion: string` — the installed
|
|
1746
|
+
`@cogenta/core` version, answered to every actor including an anonymous
|
|
1747
|
+
one (never secret), consumed by the admin footer/topbar. A caller that does
|
|
1748
|
+
not pass `cogentaVersion` gets `'0.0.0'` rather than `undefined`.
|
|
1749
|
+
|
|
1750
|
+
`@cogenta/cli` gains `getCogentaVersion()` (`commands/cogenta-version.ts`),
|
|
1751
|
+
resolving `@cogenta/core`'s own `package.json` version through
|
|
1752
|
+
`import.meta.resolve` and caching it. `cogenta serve` now threads this
|
|
1753
|
+
version into `GET /api/shell-status` and, when Cogenta's own branding stays
|
|
1754
|
+
on, into the public site footer next to its existing credit — extending
|
|
1755
|
+
`ThemeRenderOptions`'s `BrandingSettings` with an optional `cogentaVersion`
|
|
1756
|
+
field, never duplicating the branding on/off logic itself.
|
|
1757
|
+
|
|
1758
|
+
`@cogenta/theme-canonical`'s `base.css` gains a small `.cg-site-footer__version`
|
|
1759
|
+
rule for the version text above, and a `gap` on `.cg-site-footer__branding a`
|
|
1760
|
+
so the logo and the version sit apart cleanly — no structural change to the
|
|
1761
|
+
footer markup beyond the one optional `<span>`.
|
|
1762
|
+
|
|
1763
|
+
## 0.4.0
|
|
1764
|
+
|
|
1765
|
+
### Minor Changes
|
|
1766
|
+
|
|
1767
|
+
- [`d72b40f`](https://github.com/cogenta-cms/cogenta/commit/d72b40f64ab5b98985a22d9daae34796a4638f45) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Self-hosted, cookie-free page-view analytics — the one CMS feature category
|
|
1768
|
+
the audit found completely missing. No third party, no cookie, no personal
|
|
1769
|
+
data ever stored, consistent with R1 (no dure dependency on external
|
|
1770
|
+
infrastructure) and the project's privacy stance.
|
|
1771
|
+
|
|
1772
|
+
**New package `@cogenta/analytics`.** One table (`cogenta_analytics_events`):
|
|
1773
|
+
timestamp, page path, referring **domain only** (never the full referrer
|
|
1774
|
+
URL), a device category reduced from the User-Agent (`desktop`/`mobile`/
|
|
1775
|
+
`tablet`/`other`, never the raw string), and a **daily-salted session hash**
|
|
1776
|
+
— never an IP address, never a cookie. The salt (`cogenta_analytics_daily_salts`)
|
|
1777
|
+
is minted once per UTC day and rotates every day, so
|
|
1778
|
+
`sha256(salt|ip|device)` for the same real visitor is a *different*, unrelated
|
|
1779
|
+
value on every new day: nothing in the stored data can link two days of the
|
|
1780
|
+
same visitor's traffic, even with full database access, because reproducing
|
|
1781
|
+
yesterday's hash needs yesterday's IP, which was never written down. The IP
|
|
1782
|
+
address and the full User-Agent are used only as transient inputs to that
|
|
1783
|
+
hash and to the device classifier — neither is ever persisted. A dedicated
|
|
1784
|
+
privacy test suite (`test/privacy.test.ts`) inspects the actual stored
|
|
1785
|
+
columns, not just the public types, to prove this. `createAnalyticsStore`
|
|
1786
|
+
aggregates views by day, top pages, top referring domains and device
|
|
1787
|
+
breakdown; a same-session rate limit (60 events/minute) drops abusive
|
|
1788
|
+
traffic silently rather than erroring.
|
|
1789
|
+
|
|
1790
|
+
**`@cogenta/core`** gains one error code, `ANALYTICS_SALT_UNAVAILABLE`
|
|
1791
|
+
(an internal race-recovery failure, not expected in normal operation).
|
|
1792
|
+
|
|
1793
|
+
**`@cogenta/api`** gains `createAnalyticsRouter`: `GET /api/analytics/beacon`
|
|
1794
|
+
(public, records one event, always answers `204` even on a malformed or
|
|
1795
|
+
rate-limited request — a public collection endpoint must never break page
|
|
1796
|
+
rendering) and `GET /api/analytics/summary` (`admin`-only, `?days=` window).
|
|
1797
|
+
|
|
1798
|
+
**`@cogenta/cli`** wires both into `cogenta serve` and injects the collection
|
|
1799
|
+
tag into every rendered page. The tag is an invisible `<img>` pixel, not a
|
|
1800
|
+
`<script>`: the theme's rendered output already carries a hard "zero
|
|
1801
|
+
executable client JavaScript" property (enforced by a `serve.test.ts`
|
|
1802
|
+
assertion), so a script reading `document.referrer` was not an option. The
|
|
1803
|
+
referrer is instead read **server-side**, from the `Referer` header of the
|
|
1804
|
+
request that is rendering the page, and baked straight into the pixel's URL
|
|
1805
|
+
— no client code needed to capture it. The page builder's live-preview
|
|
1806
|
+
render includes the same pixel (rather than omitting it) specifically to
|
|
1807
|
+
keep its `<body>` byte-identical to the published page's, the invariant
|
|
1808
|
+
`theme-render-fidelity` depends on.
|
|
1809
|
+
|
|
1810
|
+
The admin gains a full `/analytics` dashboard (hand-built SVG bar chart, no
|
|
1811
|
+
charting dependency — R9) and a "views this week" widget on the main
|
|
1812
|
+
dashboard, both `admin`-only like every other traffic-shaped view in the
|
|
1813
|
+
admin.
|
|
1814
|
+
|
|
1815
|
+
- [`4eda357`](https://github.com/cogenta-cms/cogenta/commit/4eda35754f55484e12028707e4f54aaaccc188d2) Thanks [@georgesmomo](https://github.com/georgesmomo)! - API keys — machine-to-machine authentication, absent until now (L13 task 8).
|
|
1816
|
+
A script or integration had no way to authenticate against the REST/GraphQL
|
|
1817
|
+
API short of signing in as a human account and keeping its session alive.
|
|
1818
|
+
|
|
1819
|
+
`@cogenta/core` gains four error codes: `API_KEY_INVALID`, `API_KEY_REVOKED`,
|
|
1820
|
+
`API_KEY_EXPIRED`, `API_KEY_NOT_FOUND`.
|
|
1821
|
+
|
|
1822
|
+
`@cogenta/auth` gains `createApiKeyStore`, backed by a new
|
|
1823
|
+
`cogenta_api_keys` table that `ensureAuthTables` creates like the others. A
|
|
1824
|
+
key is `cogenta_sk_` followed by 256 bits of randomness, generated once,
|
|
1825
|
+
returned once, and never stored — only its SHA-256 hash is, looked up by
|
|
1826
|
+
that hash exactly the way `sessions.ts` looks up a session token. It is
|
|
1827
|
+
hashed fast rather than with scrypt on purpose: scrypt's cost defends a
|
|
1828
|
+
low-entropy, human-chosen secret against guessing, and a generated key has
|
|
1829
|
+
no such weakness to defend — the same reasoning that already applies to a
|
|
1830
|
+
session token.
|
|
1831
|
+
|
|
1832
|
+
A key carries an explicit `scope`: an open set of role names, exactly like a
|
|
1833
|
+
user's `roles`, chosen once at creation and never derived from the account
|
|
1834
|
+
that minted it. `AuthStore` gains `apiKeys` alongside `users`/`sessions`.
|
|
1835
|
+
|
|
1836
|
+
This changeset lands the store only. `@cogenta/api`'s `resolveActor` and the
|
|
1837
|
+
`/api/api-keys` admin router that mint and revoke keys land in a companion
|
|
1838
|
+
changeset for `@cogenta/api`/`@cogenta/cli`/`@cogenta/admin`.
|
|
1839
|
+
|
|
1840
|
+
- [`206b4cd`](https://github.com/cogenta-cms/cogenta/commit/206b4cd12df7d3a2a5831029b5f0ef726e7fd84d) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Completes the admin surface of contract E (ADR-0024) beyond its MVP: multiple
|
|
1841
|
+
variants per product, coupons, invoices and subscriptions are now all
|
|
1842
|
+
reachable from a real HTTP admin, not just the backend that already carried
|
|
1843
|
+
them.
|
|
1844
|
+
|
|
1845
|
+
`@cogenta/commerce`'s `createCommerceAdminRouter` gains: `DELETE
|
|
1846
|
+
/variants/{id}` (a product's variant list was previously append-only from the
|
|
1847
|
+
admin's point of view); `GET`/`POST /coupons` and `POST
|
|
1848
|
+
/coupons/{code}/deactivate`; `GET`/`POST /subscriptions` and the
|
|
1849
|
+
`pause`/`resume`/`cancel` actions (absent when the caller does not wire a
|
|
1850
|
+
`SubscriptionStore` — a site with no `commerceSubscriptions` store answers
|
|
1851
|
+
404, never a crash); and `GET /orders/{id}/invoice` plus `GET
|
|
1852
|
+
/orders/{id}/invoice/pdf`, the read side of an invoice-issuing route that
|
|
1853
|
+
existed but could previously only be written to, never read back. The PDF
|
|
1854
|
+
route answers with a raw `Uint8Array` body — the one response in this router
|
|
1855
|
+
that is not JSON — and the Node transport (`cogenta serve`) now checks for
|
|
1856
|
+
that shape before deciding whether to `JSON.stringify` or stream bytes with
|
|
1857
|
+
`content-type: application/pdf`.
|
|
1858
|
+
|
|
1859
|
+
`@cogenta/core` gains an optional `billing` config section (legal name,
|
|
1860
|
+
address, tax id, footer) — nothing here is a secret, rule R7 does not apply,
|
|
1861
|
+
a legal name is meant to be printed. Its absence is a real, first-class state:
|
|
1862
|
+
`cogenta serve` only builds an `InvoiceStore` and only accepts `POST
|
|
1863
|
+
/orders/{id}/invoice` once a site has filled this in, because an invoice with
|
|
1864
|
+
a made-up seller address is worse than no invoicing feature at all.
|
|
1865
|
+
|
|
1866
|
+
`@cogenta/cli` wires `createSubscriptionStore` and the conditional
|
|
1867
|
+
`createInvoiceStore` into `assembleSite`, passes `coupons`/`subscriptions`/
|
|
1868
|
+
`invoices` into the admin router (previously only `catalog`/`orders`/
|
|
1869
|
+
`customers`/`payments` were threaded through, silently dropping the coupon
|
|
1870
|
+
store `cogenta serve` already built), and adds the PDF passthrough above.
|
|
1871
|
+
|
|
1872
|
+
The admin (`@cogenta/admin`, private, no changeset) gets the screens this
|
|
1873
|
+
backend work makes possible: a real variant list per product (add, edit,
|
|
1874
|
+
remove, price and stock each independently, `commerce.catalog.write`-gated)
|
|
1875
|
+
replacing the one-variant-per-product MVP; `/commerce/coupons` (create by
|
|
1876
|
+
code/kind/value/validity window/redemption limit, deactivate); `/commerce/
|
|
1877
|
+
subscriptions` (list by status, cancel — creation is deliberately absent,
|
|
1878
|
+
since a subscription is created at checkout, not from the back office); and
|
|
1879
|
+
an "issue invoice" / download-PDF pair on the order detail screen. All money
|
|
1880
|
+
is entered and displayed through the existing `commerce/money.ts` conversion
|
|
1881
|
+
at the edges — every request on the wire still carries `priceMinor`, never a
|
|
1882
|
+
float.
|
|
1883
|
+
|
|
1884
|
+
Proven end to end in `packages/cli/test/serve-commerce.test.ts`, against a
|
|
1885
|
+
real HTTP server and a real SQLite file: a second variant added and removed
|
|
1886
|
+
through the router; a coupon created, listed and deactivated, and refused for
|
|
1887
|
+
a role with only `commerce.read`; a paid order invoiced, the invoice read
|
|
1888
|
+
back by the same route the admin polls, and its PDF downloaded and checked
|
|
1889
|
+
for the format's own magic bytes (`%PDF-`) rather than merely a 200 status; a
|
|
1890
|
+
site with no `billing` configured answering `COMMERCE_INVOICE_NOT_FOUND`
|
|
1891
|
+
instead of issuing a document with a fabricated seller address; and a
|
|
1892
|
+
subscription seeded the way checkout would seed one, listed and cancelled
|
|
1893
|
+
through the real admin API.
|
|
1894
|
+
|
|
1895
|
+
- [`03d1327`](https://github.com/cogenta-cms/cogenta/commit/03d13277224c5abd011d15e19c8f9ec67ef40c27) Thanks [@georgesmomo](https://github.com/georgesmomo)! - The other half of password reset (`.changeset/auth-password-reset.md`,
|
|
1896
|
+
L13 task 6): that changeset built the store and the terminal command and
|
|
1897
|
+
said plainly "no admin route can receive a reset click yet". This is that
|
|
1898
|
+
route, and the screen behind it.
|
|
1899
|
+
|
|
1900
|
+
`@cogenta/auth`'s `AuthStore` gains a `resets` field — the
|
|
1901
|
+
`PasswordResetStore` `createPasswordResetStore` already built, now wired
|
|
1902
|
+
into the object every caller already holds, the same way `rateLimit` and
|
|
1903
|
+
`sessions` are.
|
|
1904
|
+
|
|
1905
|
+
`@cogenta/api`'s `createAuthRouter` gains two routes. `POST
|
|
1906
|
+
/api/auth/forgot-password` accepts an email and answers with the **exact
|
|
1907
|
+
same response** whether or not an account exists for it — the line this
|
|
1908
|
+
route exists to never cross is account enumeration, and every branch of its
|
|
1909
|
+
handler (an existing account, a disabled one, a non-existent one) returns
|
|
1910
|
+
byte-identical bodies. It rate-limits by the submitted email, before the
|
|
1911
|
+
account lookup, on the same subject either way, the same posture
|
|
1912
|
+
`loginAttempts` already applies to a wrong password. Only a real, active
|
|
1913
|
+
account gets a token issued, delivered through a new optional
|
|
1914
|
+
`onForgotPassword` callback rather than a hard dependency on
|
|
1915
|
+
`@cogenta/channels` (R9) — the router itself never sends mail. `POST
|
|
1916
|
+
/api/auth/reset-password` redeems the token, sets the new password (same
|
|
1917
|
+
12-character floor as the self-service password-change route, now shared
|
|
1918
|
+
from a new `password-policy.ts` instead of duplicated), and revokes every
|
|
1919
|
+
existing session, exactly like `cogenta users reset-password --token`
|
|
1920
|
+
already does. A new error code, `AUTH_RESET_TOKEN_INVALID` (400), names an
|
|
1921
|
+
invalid, expired or already-used token — unlike `forgot-password`, this
|
|
1922
|
+
route's refusal is allowed to say why, since the secret here is the token
|
|
1923
|
+
itself, not whether an email exists.
|
|
1924
|
+
|
|
1925
|
+
`@cogenta/cli` factors the mail-sending half of `cogenta users
|
|
1926
|
+
reset-password --email` out of `commands/users.ts` into a new shared
|
|
1927
|
+
`reset-mail.ts`, so `cogenta serve` can wire the identical wording (now with
|
|
1928
|
+
an optional link to the admin's reset screen instead of the terminal
|
|
1929
|
+
command) into `onForgotPassword` without a second copy of it. `runServe`
|
|
1930
|
+
passes it to `createAuthRouter` unconditionally: the token is still issued
|
|
1931
|
+
and thrown away unsent when no site's mail is configured to go anywhere
|
|
1932
|
+
useful, since the HTTP response must never depend on whether the mail could
|
|
1933
|
+
be delivered.
|
|
1934
|
+
|
|
1935
|
+
`@cogenta/admin` (private, no changeset) gains the two screens this needed:
|
|
1936
|
+
"forgot password" on `/forgot-password`, linked from the sign-in screen, and
|
|
1937
|
+
"reset password" on `/reset-password?token=…`, the link the mail sends. Both
|
|
1938
|
+
are public routes, like `/login`. The user-management screen's role editor
|
|
1939
|
+
also moves off a raw comma-separated text field: four standard role names
|
|
1940
|
+
(`admin`/`editor`/`author`/`contributor`) are now offered as checkboxes,
|
|
1941
|
+
alongside any role a site's accounts already use, plus a free-text field for
|
|
1942
|
+
a role of the site's own — a UX convention only, not a contract A change
|
|
1943
|
+
(a role is still an arbitrary string as far as the server and the five
|
|
1944
|
+
permission actions are concerned).
|
|
1945
|
+
|
|
1946
|
+
- [`174b521`](https://github.com/cogenta-cms/cogenta/commit/174b521e9bca3b783e06ac8aa3dff6e0ded58aa5) Thanks [@georgesmomo](https://github.com/georgesmomo)! - L17 tasks 1-4: a local/embedded marketplace catalog with one-click install,
|
|
1947
|
+
scoped deliberately without a real remote registry service — L13 task 8 (API
|
|
1948
|
+
keys), which the lot names as the dependency for a distant marketplace, was
|
|
1949
|
+
never built in this repository.
|
|
1950
|
+
|
|
1951
|
+
`@cogenta/plugins` gains `createMarketplaceCatalog` (an in-memory, searchable,
|
|
1952
|
+
category-filterable directory the caller assembles — not a fetch to any
|
|
1953
|
+
external host) and `createMarketplaceInstaller`, plus `loadMarketplacePlugin`:
|
|
1954
|
+
a stricter sibling of `loadPlugin` that treats every reference as
|
|
1955
|
+
`registry`-trust unconditionally, so a marketplace item never takes the
|
|
1956
|
+
`local`/dev-mode shortcut that would otherwise skip signature verification for
|
|
1957
|
+
a catalog entry that happens to point at a local directory.
|
|
1958
|
+
|
|
1959
|
+
**The one line the whole task hinges on**: `MarketplaceInstaller.install`
|
|
1960
|
+
always calls `loadMarketplacePlugin`, which always verifies signature against
|
|
1961
|
+
the trusted registry keys — there is no parameter anywhere in this path that
|
|
1962
|
+
can skip that call, and a missing or invalid signature throws before anything
|
|
1963
|
+
is persisted. Only `kind: 'plugin'` installs for now (`MARKETPLACE_KIND_UNSUPPORTED`
|
|
1964
|
+
otherwise) — themes/skins/skills keep using their own existing registries
|
|
1965
|
+
(`createThemeRegistry`/`createSkinGallery`/`createSkillRegistry`).
|
|
1966
|
+
|
|
1967
|
+
`MarketplaceInstaller.update` re-verifies the signature of the new reference,
|
|
1968
|
+
computes newly-declared capabilities against the plugin's existing grants
|
|
1969
|
+
(`detectCapabilitiesNeedingApproval`, unchanged from L7), and refuses
|
|
1970
|
+
(`MARKETPLACE_UPDATE_REQUIRES_APPROVAL`) unless the caller explicitly passes
|
|
1971
|
+
`confirmPendingPermissions: true` — and even then, no capability is
|
|
1972
|
+
auto-granted; `PluginGrantStore.grant` stays a separate, explicit step.
|
|
1973
|
+
|
|
1974
|
+
`@cogenta/api` gains `createMarketplaceRouter` (`/api/marketplace/items`,
|
|
1975
|
+
admin-only, structurally typed against `@cogenta/plugins` rather than
|
|
1976
|
+
depending on it at runtime) with list/detail/install/update/uninstall routes.
|
|
1977
|
+
The detail route reuses `describeCapability` (L7 task 7) so a plugin's
|
|
1978
|
+
requested capabilities read in plain language, the same sentences the
|
|
1979
|
+
existing permission-review screen already renders.
|
|
1980
|
+
|
|
1981
|
+
`@cogenta/core` gains the error codes this needs:
|
|
1982
|
+
`MARKETPLACE_ITEM_NOT_FOUND`, `MARKETPLACE_KIND_UNSUPPORTED`,
|
|
1983
|
+
`MARKETPLACE_ALREADY_INSTALLED`, `MARKETPLACE_NOT_INSTALLED`,
|
|
1984
|
+
`MARKETPLACE_UPDATE_REQUIRES_APPROVAL` — and `PLUGIN_SIGNATURE_MISSING`/
|
|
1985
|
+
`PLUGIN_SIGNATURE_INVALID`/`PLUGIN_SOURCE_NOT_FOUND`/`PLUGIN_MANIFEST_INVALID`
|
|
1986
|
+
(existing L7 codes, never before mapped to an HTTP status because no REST
|
|
1987
|
+
route threw them until now) gain entries in `statusFor` (422/404/422).
|
|
1988
|
+
|
|
1989
|
+
**Not done, by explicit scope cut under a hard deadline**: `cogenta serve`
|
|
1990
|
+
does not yet mount this router, so the catalog/installer above are complete,
|
|
1991
|
+
independently tested, and ready to wire, but not yet reachable over HTTP from
|
|
1992
|
+
a running site — the same honest gap the codebase already tolerates elsewhere
|
|
1993
|
+
(`cogenta build`/`deploy`/`theme`, L9 task 9) rather than a stub. Bundled
|
|
1994
|
+
updates across multiple items and the commercial (paid extension) track named
|
|
1995
|
+
in the lot doc are both out of scope for this pass.
|
|
1996
|
+
|
|
1997
|
+
- [`b37e51c`](https://github.com/cogenta-cms/cogenta/commit/b37e51cea79fc8d3070d5c741a8415192985d9ff) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Give the redirect table, HTTP security and outbound webhooks a real admin
|
|
1998
|
+
screen (audit follow-up to L10 tasks 2/6 and L14 task 1)
|
|
1999
|
+
|
|
2000
|
+
Three backend pieces existed and were fully wired into `cogenta serve` with
|
|
2001
|
+
no way to reach them from a browser.
|
|
2002
|
+
|
|
2003
|
+
- `@cogenta/core` gains the `REDIRECT_UNKNOWN` error code, for a `DELETE` on a
|
|
2004
|
+
redirect that does not exist.
|
|
2005
|
+
- `@cogenta/api` gains `createRedirectRouter` (`GET`/`POST`/`DELETE
|
|
2006
|
+
/api/redirects`) and `createOpsStatusRouter` (`GET /api/security-status`,
|
|
2007
|
+
`GET /api/webhooks-status`). Both are admin-only on every method, including
|
|
2008
|
+
`GET`: a redirect table and a site's CORS/CSP/HSTS configuration are
|
|
2009
|
+
routing and hardening decisions, not content, so neither has a reader role
|
|
2010
|
+
the way a taxonomy or a menu does. Loop and self-redirect refusal is
|
|
2011
|
+
entirely `RedirectStore`'s own job (`CONTENT_REDIRECT_LOOP`,
|
|
2012
|
+
`CONTENT_ROUTE_INVALID`), surfaced here as a proper 409/400 instead of a
|
|
2013
|
+
500.
|
|
2014
|
+
- `cogenta serve` mounts all three at `/api/redirects`, `/api/security-status`
|
|
2015
|
+
and `/api/webhooks-status`, and `@cogenta/admin` gains three screens:
|
|
2016
|
+
`/redirects` (full CRUD) and `/ops-settings` (`security` and `webhooks`,
|
|
2017
|
+
**read-only**).
|
|
2018
|
+
|
|
2019
|
+
The security and webhooks screens are read-only by design, not by omission.
|
|
2020
|
+
Both settings live in the site's `cogenta.config.mjs` — versioned in git,
|
|
2021
|
+
deployed with the code that depends on it (a CSP that allows a script host
|
|
2022
|
+
has to travel with the deploy that added the script). Letting the admin edit
|
|
2023
|
+
them would create a second source of truth that disagrees with the file the
|
|
2024
|
+
moment either one changes without the other, which is a bigger architecture
|
|
2025
|
+
change than this audit's scope. The screens instead mirror exactly what the
|
|
2026
|
+
running process is enforcing on every request.
|
|
2027
|
+
|
|
2028
|
+
No delivery history is shown for webhooks: none is persisted anywhere today
|
|
2029
|
+
(`WebhookEventSender.send` only ever returns a per-call result to log). The
|
|
2030
|
+
screen says so rather than inventing one.
|
|
2031
|
+
|
|
2032
|
+
## 0.3.0
|
|
2033
|
+
|
|
2034
|
+
### Minor Changes
|
|
2035
|
+
|
|
2036
|
+
- [`552645e`](https://github.com/cogenta-cms/cogenta/commit/552645e039b8c8c4f5340d065ea2f4a552950815) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Advanced AI (L18): a writing assistant, a `vector` driver, semantic search,
|
|
2037
|
+
RAG chat with citations, classification/duplicate detection/moderation, and
|
|
2038
|
+
FAQ/Schema.org drafting. **Nothing here is on a required path** — a site with
|
|
2039
|
+
no AI provider configured behaves exactly as before, and the whole feature set
|
|
2040
|
+
disappears from the UI rather than failing (R2).
|
|
2041
|
+
|
|
2042
|
+
- **`@cogenta/agents`** gains the `vector` driver need the architecture
|
|
2043
|
+
document has named since L0 and nothing implemented: `VectorStore` with three
|
|
2044
|
+
drivers behind the existing `createDriverRegistry` — `pgvector` (optimal),
|
|
2045
|
+
`file` (degraded, survives a restart) and `memory` (degraded, always
|
|
2046
|
+
available). One contract suite runs against all three; pgvector's run is an
|
|
2047
|
+
integration test that skips loudly without `COGENTA_TEST_POSTGRES_URL`.
|
|
2048
|
+
Nothing re-implements cosine similarity: L4's `vectorRank` does the ranking
|
|
2049
|
+
everywhere, and all three drivers return the same number.
|
|
2050
|
+
|
|
2051
|
+
`createSemanticSearch` fuses the vector half with L10's full-text index by
|
|
2052
|
+
RRF — **beside it, never instead of it**: pure vector search misses
|
|
2053
|
+
exact-keyword queries, which is the failure the architecture document warns
|
|
2054
|
+
about at line 190.
|
|
2055
|
+
|
|
2056
|
+
Fifteen Contract C tools, all `sideEffects: false`, every output carrying
|
|
2057
|
+
`applied: false` as a **literal** so an assistant tool's type cannot say it
|
|
2058
|
+
changed anything (R6). Eight writing tools (rewrite, proofread, summarise,
|
|
2059
|
+
translate, meta description, titles, tags, alt text), `assist.generate_image`
|
|
2060
|
+
behind a two-vendor image provider driver (OpenAI, Stability), `assist.chat`
|
|
2061
|
+
(RAG with citations), `assist.classify`/`assist.find_duplicates`/
|
|
2062
|
+
`assist.moderate`, and `assist.faq_draft`/`assist.schema_org_draft`.
|
|
2063
|
+
|
|
2064
|
+
Three properties worth knowing:
|
|
2065
|
+
- **Citations come from retrieval, not from the model.** The model names
|
|
2066
|
+
1-based indices into the passages it was shown; this code maps them back to
|
|
2067
|
+
what the retriever returned, and an invented index resolves to nothing. A
|
|
2068
|
+
chat answer can never cite a page that was not retrieved.
|
|
2069
|
+
- **Moderation and duplicate detection can recommend `none` or `review`, and
|
|
2070
|
+
nothing else.** The union has no destructive member, so no answer —
|
|
2071
|
+
however jailbroken — describes a deletion.
|
|
2072
|
+
- **`assist.find_duplicates` needs no AI provider at all.** It embeds with
|
|
2073
|
+
the site's `EmbeddingProvider`, which by default is the local hashing one:
|
|
2074
|
+
no key, no service, no model download.
|
|
2075
|
+
|
|
2076
|
+
- **`@cogenta/core`** gains an `imageGeneration` config section
|
|
2077
|
+
(`COGENTA_IMAGE_PROVIDER`/`_MODEL`/`_BASE_URL`, key in `COGENTA_IMAGE_API_KEY`
|
|
2078
|
+
and refused in the config file like every other secret), a `vector` section
|
|
2079
|
+
(`driver`/`path`/`table` — dimensions stay on `embeddings`, never duplicated),
|
|
2080
|
+
and the error codes `VECTOR_DIMENSION_MISMATCH`, `VECTOR_STORE_FAILED`,
|
|
2081
|
+
`ASSIST_UNAVAILABLE`, `ASSIST_RESPONSE_INVALID`.
|
|
2082
|
+
|
|
2083
|
+
- **`@cogenta/api`** gains `createAssistantRouter` — `GET /api/assistant` and
|
|
2084
|
+
`POST /api/assistant/run`. The `GET` answers **200 with
|
|
2085
|
+
`{available: false, tools: []}`** on a site with no provider, which is what
|
|
2086
|
+
lets a client render nothing instead of handling an error. The permission
|
|
2087
|
+
gate is the route's, not the tools' (R4): an actor may use the assistant when
|
|
2088
|
+
they may edit content somewhere, and an anonymous caller is refused before any
|
|
2089
|
+
provider is contacted, so an unauthenticated request can never spend the
|
|
2090
|
+
site's AI budget. The route also refuses any tool declaring a side effect,
|
|
2091
|
+
even though none does.
|
|
2092
|
+
|
|
2093
|
+
- **`@cogenta/cli`** wires all of it into `cogenta serve`: providers built from
|
|
2094
|
+
the config, the vector store selected through the registry, the content stores
|
|
2095
|
+
wrapped so a publish updates the embedding index the same way it already
|
|
2096
|
+
updates the full-text one, and `/api/assistant` mounted on every site. Every
|
|
2097
|
+
piece degrades to "off" with a log line rather than stopping the site: an
|
|
2098
|
+
unknown provider name, a missing API key, an unavailable vector store and an
|
|
2099
|
+
embeddings provider with no adapter yet are four warnings, not four crashes.
|
|
2100
|
+
|
|
2101
|
+
**Migration**: none. Every new configuration section is optional, and a site
|
|
2102
|
+
that adds none behaves exactly as it did before.
|
|
2103
|
+
|
|
2104
|
+
- [`8b561d1`](https://github.com/cogenta-cms/cogenta/commit/8b561d1ba735eb2b42c27725f67faf64e53866e5) Thanks [@georgesmomo](https://github.com/georgesmomo)! - E-commerce (L15), as a new package `@cogenta/commerce` on a **new contract E**
|
|
2105
|
+
rather than an extension of contract A.
|
|
2106
|
+
|
|
2107
|
+
The decision is proposed in `ADR-DRAFT-commerce.md` at the repo root and is
|
|
2108
|
+
**not yet acted** — it needs a human to accept it before it goes into
|
|
2109
|
+
`docs/03-decisions.md`. The implementation assumes it. In one line: three of
|
|
2110
|
+
contract A's own decisions make an order a bad content entry. ADR-0014 would
|
|
2111
|
+
fork one order per language; ADR-0022 has just made every content entry
|
|
2112
|
+
restorable from the trash, and an order is not; and versioning drafts have no
|
|
2113
|
+
meaning for a sale. The product's *editorial* face stays firmly in contract A
|
|
2114
|
+
through an optional `contentRef`, so a catalogue keeps rich text, blocks, SEO,
|
|
2115
|
+
translations and scheduling for free. Contract A does not move: it stays at
|
|
2116
|
+
`schema@2.0`, and a site that sells nothing never creates a commerce table.
|
|
2117
|
+
|
|
2118
|
+
**Money is an integer of minor units, everywhere.** The three mandatory
|
|
2119
|
+
dialects do not agree on decimals — SQLite has only `REAL`, a binary float — so
|
|
2120
|
+
a decimal column would mean something subtly different on one of the three
|
|
2121
|
+
supported databases. Rates are basis points for the same reason. Amounts are
|
|
2122
|
+
`bigint` columns, and every read goes through a decoder, because `pg` hands
|
|
2123
|
+
`int8` back as a *string*: a price read as `"1999"` and added to another is
|
|
2124
|
+
`"19991999"`, a bug that would appear only on Postgres and only in production.
|
|
2125
|
+
|
|
2126
|
+
**Stock cannot go negative.** `takeStock` runs one immediate transaction and
|
|
2127
|
+
lowers each line with `update … set on_hand = on_hand - n where id = ? and
|
|
2128
|
+
on_hand >= n`, reading `rowsAffected` — the same idiom that makes a password
|
|
2129
|
+
reset token single use. Repeated variants in one basket are summed first, or
|
|
2130
|
+
two lines of two would each pass against a stock of three. The concurrency test
|
|
2131
|
+
is a real race against a SQLite *file* with two independent connections
|
|
2132
|
+
(`:memory:` gives two unrelated databases and would prove nothing), and it
|
|
2133
|
+
carries a control that re-implements the naive read-then-write and asserts it
|
|
2134
|
+
*does* oversell.
|
|
2135
|
+
|
|
2136
|
+
**Placing an order is one transaction**: stock taken, coupon redemption
|
|
2137
|
+
claimed, order and lines written, cart closed, first history event recorded.
|
|
2138
|
+
Any failure and none of it happened.
|
|
2139
|
+
|
|
2140
|
+
**Payment is interface plus two implementations**, like cache, queue and
|
|
2141
|
+
storage (R1). Stripe is `optimal`, written against the REST API with `fetch`
|
|
2142
|
+
and no `stripe` dependency, with real webhook signature verification
|
|
2143
|
+
(timing-safe, every `v1` candidate, 5-minute freshness window). Bank transfer
|
|
2144
|
+
is `degraded` and is **not a stub** — plenty of businesses are paid that way
|
|
2145
|
+
and nothing else; the difference is who confirms the money arrived. The whole
|
|
2146
|
+
checkout, the whole subscription biller and the whole invoice path are tested
|
|
2147
|
+
end to end with no API key, URL or network configured anywhere (R2).
|
|
2148
|
+
|
|
2149
|
+
**Invoice numbers are gapless and never reused**, claimed by a compare-and-set
|
|
2150
|
+
inside the transaction that writes the invoice, so a rolled-back invoice does
|
|
2151
|
+
not burn a number and two invoices issued in the same millisecond get
|
|
2152
|
+
consecutive ones. A `count(*) + 1` would hand out duplicates under any
|
|
2153
|
+
concurrency and re-issue a number a deleted row used to hold. The PDF is
|
|
2154
|
+
generated with zero dependencies (R9/R10) and is deterministic: the same
|
|
2155
|
+
invoice regenerated years later is byte-identical, because it renders from a
|
|
2156
|
+
frozen snapshot and never reads a clock.
|
|
2157
|
+
|
|
2158
|
+
Also: tax rules resolved by specificity rather than insertion order; shipping
|
|
2159
|
+
methods with an optional carrier driver that falls back to the stored rate when
|
|
2160
|
+
the courier's API is down; coupons with three kinds and a redemption count
|
|
2161
|
+
claimed the same way stock is; subscriptions whose month arithmetic puts 31
|
|
2162
|
+
January + 1 month on 28 February rather than 3 March, and which bill through
|
|
2163
|
+
the same orders, payments and invoices as everything else.
|
|
2164
|
+
|
|
2165
|
+
`@cogenta/core` gains the `COMMERCE_*` error codes (a minor bump: adding a code
|
|
2166
|
+
is additive, and nothing existing changed meaning).
|
|
2167
|
+
|
|
2168
|
+
**Not in this release**, and deliberately so: no admin React screens (the
|
|
2169
|
+
back office is a transport-free router with its own permission vocabulary,
|
|
2170
|
+
tested by role — the UI belongs with L11's design system), no storefront
|
|
2171
|
+
blocks, and no Stripe integration test against a real sandbox (it is written
|
|
2172
|
+
and skips loudly without `COGENTA_TEST_STRIPE_SECRET_KEY`).
|
|
2173
|
+
|
|
2174
|
+
- [`182ef48`](https://github.com/cogenta-cms/cogenta/commit/182ef48d97e2757e7b1404dc407327f53ed377dd) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Document text extraction, as a contract C tool (L19 task 1). `@cogenta/agents`
|
|
2175
|
+
gains `document.extract_text` and the `extractDocumentText` function behind it:
|
|
2176
|
+
PDF, DOCX, Markdown and plain text in, plain text out. Format detection reads
|
|
2177
|
+
the bytes rather than the extension, since a brief emailed as `.pdf` is often
|
|
2178
|
+
really a `.docx`.
|
|
2179
|
+
|
|
2180
|
+
No new dependency, on purpose (R9/R10). A `.docx` is a ZIP whose
|
|
2181
|
+
`word/document.xml` holds the body, and `node:zlib` already opens it — the
|
|
2182
|
+
~120 lines of central-directory reading here replace a callback-era unzip
|
|
2183
|
+
library. The PDF reader walks content streams and their text-showing
|
|
2184
|
+
operators (`Tj`, `TJ`, `'`, `"`) instead of pulling in `pdf.js` through
|
|
2185
|
+
`pdf-parse`.
|
|
2186
|
+
|
|
2187
|
+
It refuses rather than guesses, which is the part that matters downstream: a
|
|
2188
|
+
scan with no text layer is `DOCUMENT_NO_TEXT_LAYER`, an encrypted PDF says so,
|
|
2189
|
+
a legacy binary `.doc` is named as such, and — calibrated against real
|
|
2190
|
+
LaTeX-exported specifications — a PDF whose text layer is subset-font glyph
|
|
2191
|
+
indices is refused too, rather than passing mojibake on to an agent that would
|
|
2192
|
+
happily build a confident, entirely invented site plan from it. Footnotes and
|
|
2193
|
+
endnotes of a `.docx` are appended rather than dropped, and an embedded image
|
|
2194
|
+
produces a warning saying any requirement written inside it was not read.
|
|
2195
|
+
|
|
2196
|
+
`@cogenta/core` gains the error codes this needs
|
|
2197
|
+
(`DOCUMENT_FORMAT_UNSUPPORTED`, `DOCUMENT_TOO_LARGE`,
|
|
2198
|
+
`DOCUMENT_EXTRACTION_FAILED`, `DOCUMENT_NO_TEXT_LAYER`) plus the ones L19's
|
|
2199
|
+
later tasks use.
|
|
2200
|
+
|
|
2201
|
+
Contract C moves to `tools@1.1`: the permission taxonomy gains
|
|
2202
|
+
`document.extract`. No existing tool signature changes.
|
|
2203
|
+
|
|
2204
|
+
- [`755201d`](https://github.com/cogenta-cms/cogenta/commit/755201d55fd8c04ba2794a03797696769b59f6cc) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Send a real signed webhook when content is published (L14 task 1)
|
|
2205
|
+
|
|
2206
|
+
The signed outbound webhook channel has existed since L6 and nothing ever
|
|
2207
|
+
called it. It is now connected to the content lifecycle.
|
|
2208
|
+
|
|
2209
|
+
- `@cogenta/channels` gains `createWebhookEventSender`, which POSTs a
|
|
2210
|
+
structured `{ event, occurredAt, data }` envelope to every configured
|
|
2211
|
+
endpoint. It reuses `signOutgoingWebhook` and the existing
|
|
2212
|
+
`X-Cogenta-Timestamp` / `X-Cogenta-Signature` headers **verbatim**, so a
|
|
2213
|
+
receiver verifies an event with `verifyIncomingWebhook` exactly as it
|
|
2214
|
+
verifies a message — there is no second signing path. It never throws: a
|
|
2215
|
+
failed delivery comes back as a result to log, so an editor's publish is
|
|
2216
|
+
never lost to somebody else's downtime.
|
|
2217
|
+
- `@cogenta/schema` gains `withLifecycleEvents`, a `ContentStore` decorator in
|
|
2218
|
+
the same shape as `withSearchIndexing`. It emits `content.publish` (from
|
|
2219
|
+
`publish()`, and from `create()` with a published status),
|
|
2220
|
+
`content.unpublish` and `content.delete`, each carrying the entry's
|
|
2221
|
+
identity, status, timestamps and its real route path from `buildPath`.
|
|
2222
|
+
Draft edits emit nothing. The event body never carries the content itself.
|
|
2223
|
+
- `@cogenta/core` gains a `webhooks.endpoints` config section. The signing
|
|
2224
|
+
secret is environment-only (`COGENTA_WEBHOOK_SECRET`, rule R7); endpoints
|
|
2225
|
+
configured without it disable delivery with a startup warning rather than
|
|
2226
|
+
falling back to unsigned requests.
|
|
2227
|
+
- `cogenta serve` wires the two together, outermost of all store decorators so
|
|
2228
|
+
an event only describes a write that really landed.
|
|
2229
|
+
|
|
2230
|
+
Proven end to end by a suite that publishes over real HTTP and verifies the
|
|
2231
|
+
signature on the bytes a real `node:http` receiver got off the socket.
|
|
2232
|
+
|
|
2233
|
+
- [`551a06c`](https://github.com/cogenta-cms/cogenta/commit/551a06c2e58bb4119618e5502dfcae4bb024b7d4) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Serve a site's own page for an unmatched URL (L14 task 2)
|
|
2234
|
+
|
|
2235
|
+
`cogenta serve` answered every unmatched public URL with a bare JSON error.
|
|
2236
|
+
It now renders the site's own 404 page instead, with a real 404 status.
|
|
2237
|
+
|
|
2238
|
+
The 404 body is an ordinary published entry at `site.notFoundPath` (`/404` by
|
|
2239
|
+
default, overridable in `cogenta.config` or via `COGENTA_SITE_NOT_FOUND_PATH`)
|
|
2240
|
+
— editable in the admin like any other page, and rendered by exactly the same
|
|
2241
|
+
function, through exactly the same permission-checked gateway, as every other
|
|
2242
|
+
page. So a draft 404 page is not shown to the public, and a site that has not
|
|
2243
|
+
written one still gets the plain refusal it got before. The lookup happens at
|
|
2244
|
+
most once per request: the 404 path itself is never re-resolved.
|
|
2245
|
+
|
|
2246
|
+
- [`87bae8d`](https://github.com/cogenta-cms/cogenta/commit/87bae8dd4cc08261f3d5ba83947fa2ad77b0b826) Thanks [@georgesmomo](https://github.com/georgesmomo)! - **Breaking: contract A moves to `schema@2.0`** (ADR-0022) — the trash and native
|
|
2247
|
+
taxonomies, in one version bump with one migration.
|
|
2248
|
+
|
|
2249
|
+
### `delete()` changed meaning without changing signature
|
|
2250
|
+
|
|
2251
|
+
`ContentStore.delete()` no longer issues a `DELETE`. It writes the new system
|
|
2252
|
+
field `deletedAt` and leaves every row where it was — versions, blocks, join
|
|
2253
|
+
rows, and the `translation_of` of any translation. Two new methods complete it:
|
|
2254
|
+
|
|
2255
|
+
- `purge(id)` is the real `DELETE`, i.e. what `delete()` used to do;
|
|
2256
|
+
- `untrash(id)` takes an entry back out, with the status it went in with;
|
|
2257
|
+
- `purgeExpired()` removes what has outlived the collection's `trash.retainDays`.
|
|
2258
|
+
|
|
2259
|
+
**How to migrate.** Code that called `delete()` to genuinely destroy a row — an
|
|
2260
|
+
import script that cleans up, a test that resets — must now call `purge()`.
|
|
2261
|
+
Nothing will fail loudly if you do not: the call still succeeds and simply
|
|
2262
|
+
leaves the row behind, which is the worst kind of break and the reason it is
|
|
2263
|
+
called out first here. `trash: false` on a collection restores the old
|
|
2264
|
+
behaviour outright.
|
|
2265
|
+
|
|
2266
|
+
### Every read now filters the trash by default
|
|
2267
|
+
|
|
2268
|
+
`read`, `list`, `translations`, `resolveLocale` and `history` exclude trashed
|
|
2269
|
+
entries unless the caller passes `trashed: 'include' | 'only'`. That direction
|
|
2270
|
+
is deliberate: a renderer, a sitemap or a headless client written against 1.0
|
|
2271
|
+
keeps serving live content with no change at all.
|
|
2272
|
+
|
|
2273
|
+
### `restrict` is now enforced in application code
|
|
2274
|
+
|
|
2275
|
+
Trashing is an `UPDATE`, so a foreign key can no longer refuse it. `delete()`
|
|
2276
|
+
checks referring entries itself and names what blocks ("2 entries of
|
|
2277
|
+
\"article\" still reference it"); `purge()` runs the same check so both paths
|
|
2278
|
+
give the same sentence. This needs the sibling collections, so
|
|
2279
|
+
`createContentStore` takes a new optional `siblings` option — **pass it**. Left
|
|
2280
|
+
out, only self-references are checked; nothing is destroyed, since `purge()`
|
|
2281
|
+
still meets the real foreign key, but a trash that should have been refused
|
|
2282
|
+
will be allowed.
|
|
2283
|
+
|
|
2284
|
+
`withReadOnlyStore` refuses `delete`, `untrash`, `purge` and `purgeExpired`.
|
|
2285
|
+
|
|
2286
|
+
### Native taxonomies
|
|
2287
|
+
|
|
2288
|
+
`defineTaxonomy()` is a second top-level declarable object beside
|
|
2289
|
+
`defineCollection()`, and `f.taxonomy({ of, many })` a new field kind. A term
|
|
2290
|
+
carries `id`, `parent`, `slug`, `position` and `labels` indexed by locale, and
|
|
2291
|
+
deliberately no `status`, `version` or `translationOf`: a classification is not
|
|
2292
|
+
content, so ADR-0014 does not govern it.
|
|
2293
|
+
|
|
2294
|
+
The tree is stored as a **materialised path** maintained on write, never a
|
|
2295
|
+
recursive CTE: "everything under this term" is one `like` that Postgres,
|
|
2296
|
+
MySQL/MariaDB and SQLite answer identically (ADR-0006). Paths are built from
|
|
2297
|
+
ids, so renaming a term rewrites nothing and only a move pays. Nesting is
|
|
2298
|
+
bounded at 12 levels so the indexed column stays inside InnoDB's key limit.
|
|
2299
|
+
|
|
2300
|
+
`createTaxonomyStore()` is the term store; `createSchemaTables(db, collections,
|
|
2301
|
+
taxonomies)` and `dropSchemaTables` take the taxonomies as a third argument.
|
|
2302
|
+
|
|
2303
|
+
### The migration
|
|
2304
|
+
|
|
2305
|
+
`schema2Migration({ collections, taxonomies })` adds `deleted_at` to every
|
|
2306
|
+
entry table and creates the terms and join tables. It is marked **destructive**,
|
|
2307
|
+
so the migrator demands an explicit confirmation and a verified backup: its
|
|
2308
|
+
`down` drops `deleted_at` and the terms tables, which permanently discards
|
|
2309
|
+
everything in the trash and every classification — entries sitting in the trash
|
|
2310
|
+
silently become live again with no record they were ever deleted.
|
|
2311
|
+
|
|
2312
|
+
### Also
|
|
2313
|
+
|
|
2314
|
+
`.cogenta/schema.json` reports `schema@2.0`, carries the declared taxonomies and
|
|
2315
|
+
each collection's trash window, and `buildSchemaDocument`/`renderSchemaJson`
|
|
2316
|
+
take the taxonomies. `@cogenta/core` gains the error codes the two features
|
|
2317
|
+
need: `CONTENT_REFERENCED`, `CONTENT_NOT_TRASHED` and the `TAXONOMY_*` family.
|
|
2318
|
+
|
|
2319
|
+
- [`ca71b3b`](https://github.com/cogenta-cms/cogenta/commit/ca71b3bbd5d5d7371923d0521444fc94a525de06) Thanks [@georgesmomo](https://github.com/georgesmomo)! - CORS, security headers and a coherent cache-control on `cogenta serve`
|
|
2320
|
+
(L10 task 6).
|
|
2321
|
+
|
|
2322
|
+
`@cogenta/core`'s configuration gains a `security` section:
|
|
2323
|
+
|
|
2324
|
+
```ts
|
|
2325
|
+
security: {
|
|
2326
|
+
cors: { origins: ['https://app.example.com'], credentials: false },
|
|
2327
|
+
csp: "default-src 'self'",
|
|
2328
|
+
hstsMaxAge: 31536000,
|
|
2329
|
+
pageMaxAge: 60,
|
|
2330
|
+
}
|
|
2331
|
+
```
|
|
2332
|
+
|
|
2333
|
+
Every field is off or permissive-by-omission by default, and that is a
|
|
2334
|
+
decision rather than timidity. CORS is off unless a site names an origin —
|
|
2335
|
+
the origin list *is* the switch, so "CORS is on" and "these origins may read
|
|
2336
|
+
it" cannot drift apart. HSTS is off unless asked and is never sent over plain
|
|
2337
|
+
HTTP: on a host that is not fully HTTPS it locks browsers out for `maxAge`
|
|
2338
|
+
seconds with no server-side undo, and it is the one header a wrong default can
|
|
2339
|
+
take a site offline with. Credentials together with the `*` origin is refused
|
|
2340
|
+
at startup, because every browser refuses that pair and a server that accepted
|
|
2341
|
+
it would look configured while granting nothing.
|
|
2342
|
+
|
|
2343
|
+
`cogenta serve` applies all of it in one place, before any route runs, so a
|
|
2344
|
+
route added later cannot opt out by forgetting:
|
|
2345
|
+
|
|
2346
|
+
- `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN` and
|
|
2347
|
+
`Referrer-Policy: strict-origin-when-cross-origin` on every response.
|
|
2348
|
+
- The configured CSP verbatim — a string, not a builder, because a CSP depends
|
|
2349
|
+
on which analytics, fonts and embeds a site actually uses.
|
|
2350
|
+
- CORS with an echoed (never blindly reflected) origin and `Vary: Origin`,
|
|
2351
|
+
plus a real preflight answer.
|
|
2352
|
+
- Cache-control by path class: `no-store` for `/api/*` and for the admin,
|
|
2353
|
+
`public, max-age=0, s-maxage=<pageMaxAge>, must-revalidate` for a public
|
|
2354
|
+
page, and the long immutable value image variants already set for
|
|
2355
|
+
themselves.
|
|
2356
|
+
|
|
2357
|
+
### Patch Changes
|
|
2358
|
+
|
|
2359
|
+
- [`6ad0f3a`](https://github.com/cogenta-cms/cogenta/commit/6ad0f3a495176169fe95f4955dfef30a6af376fd) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Closes four denial-of-service and permission-escalation gaps a security review found in L19's document-upload pipeline and site-plan review screen, all reachable from a single uploaded file or a proposed content model — no LLM provider required to trigger them.
|
|
2360
|
+
|
|
2361
|
+
- `.docx` extraction (`packages/agents/src/documents/docx.ts`): the regex scanning `word/document.xml` for `<w:t>…</w:t>` runs backtracked quadratically on unclosed tags (measured 21.8 s for 400 KB). Replaced with a single linear `indexOf`-based scan, and `word/document.xml`/footnotes/endnotes are now capped at 8 MiB each (`zip.ts`'s `read()` gained a per-call `maxBytes`) instead of the shared 200 MiB decompression-bomb ceiling, since a highly repetitive XML payload can deflate at several hundred to one.
|
|
2362
|
+
- PDF stream collection (`packages/agents/src/documents/pdf.ts`): `collectStreams` used an unbounded `lastIndexOf` to find each stream's dictionary, which re-scans the entire prefix of the file for every stream found — a file that is mostly fake `stream`/`endstream` markers with no real PDF structure could cost minutes of CPU with no decompression involved. The search window is now bounded to 2 KiB behind each `stream` keyword, and the number of streams processed is capped at 10 000.
|
|
2363
|
+
- PDF text accumulation (`packages/agents/src/documents/pdf.ts`, `extract-text.ts`): `MAX_TEXT_CHARACTERS` was only enforced after every content stream had already been decoded and joined, so a PDF with many individually-small-enough, highly compressible streams could accumulate many times that budget in memory before truncation ever ran. The reader now stops pulling in further pages once the accumulated text already exceeds the cap, moved to a shared `limits.ts` so both `pdf.ts` and `extract-text.ts` read the same number.
|
|
2364
|
+
- Site plan review (`packages/agents/src/site-plan/content-model.ts`, `approval.ts`): a proposed content model's `permissions` is entirely the model's own choice, so a hallucinated or prompt-injected proposal granting `public` the `create`/`update`/`delete` actions would have let any anonymous visitor write to that collection once the plan was applied. `buildCollection` now refuses such a proposal outright (`CONTENT_MODEL_PROPOSAL_PERMISSIONS_UNSAFE`, fed back as the next attempt's correction like any other invalid proposal); separately, the human review screen (`summarisePlan`) now always shows a collection's proposed permissions and routing pattern, not only its fields and rationale, so a legitimate-but-surprising grant is visible before acceptance.
|
|
2365
|
+
- `cogenta serve` (`packages/cli/src/commands/serve.ts`): `readBody` had no byte limit, and the one route inviting multi-megabyte bodies by design (`/api/site-plans`) only checked the admin role after the body was fully buffered. `readBody` now caps every request body at 64 MiB, rejecting with a new `REQUEST_BODY_TOO_LARGE` error code (HTTP 413); `/api/site-plans` now checks the admin role before reading the body at all, so a non-admin caller — anonymous or not — is turned away before the server reads anything they sent.
|
|
2366
|
+
|
|
2367
|
+
## 0.2.0
|
|
2368
|
+
|
|
2369
|
+
### Minor Changes
|
|
2370
|
+
|
|
2371
|
+
- [`4c95475`](https://github.com/cogenta-cms/cogenta/commit/4c9547543ec9a4464d8c9a05d1967dd15b7953aa) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `THEME_IMAGE_UNSUPPORTED`, thrown by `cogenta serve`'s new theme-render
|
|
2372
|
+
fallback (`@cogenta/cli`) when a theme block asks for an image — no image
|
|
2373
|
+
pipeline is wired into that in-process fallback yet, so a theme gets a clear,
|
|
2374
|
+
typed refusal rather than a broken `<img>`.
|
|
2375
|
+
|
|
2376
|
+
### Patch Changes
|
|
2377
|
+
|
|
2378
|
+
- [`fd0a52e`](https://github.com/cogenta-cms/cogenta/commit/fd0a52e155d802b102ac9012b3ed2d650b271c3f) Thanks [@georgesmomo](https://github.com/georgesmomo)! - `loadConfig` now auto-loads a `.env` file next to `cogenta.config.mjs`, using
|
|
2379
|
+
Node's own `process.loadEnvFile` (no new dependency, R9) — so a real secret
|
|
2380
|
+
like `COGENTA_AUTH_SIGNING_KEY` no longer has to be exported by hand in every
|
|
2381
|
+
shell before `cogenta serve` will start. Skipped whenever the resolved `env`
|
|
2382
|
+
is not really `process.env` (identity check, not `options.env === undefined`
|
|
2383
|
+
— real callers like the CLI's own `run()` resolve `options.env ?? process.env`
|
|
2384
|
+
once and thread that same object down explicitly, so `options.env` is
|
|
2385
|
+
"defined" even in a real, unconfigured shell; a test injecting its own
|
|
2386
|
+
synthetic map is still exempt, since that map is a different object).
|
|
2387
|
+
|
|
2388
|
+
Found via the user's own real end-to-end test: `npx cogenta serve` refused to
|
|
2389
|
+
start with "COGENTA_AUTH_SIGNING_KEY is not set", and the only documented fix
|
|
2390
|
+
was a manual, shell-specific `export`/`$env:` command with no Windows/Mac/
|
|
2391
|
+
Linux guidance. `create-cogenta` now writes a real generated key into `.env`
|
|
2392
|
+
(see `create-cogenta`'s own changeset) — this is the half of the fix that
|
|
2393
|
+
makes `cogenta serve` actually read it back.
|
|
2394
|
+
|
|
2395
|
+
## 0.1.0
|
|
2396
|
+
|
|
2397
|
+
### Minor Changes
|
|
2398
|
+
|
|
2399
|
+
- [`f323580`](https://github.com/cogenta-cms/cogenta/commit/f3235809422e16a4e9d34f16e1171d2ebcfaf01a) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `@cogenta/cli` and its first command, `cogenta doctor`.
|
|
2400
|
+
|
|
2401
|
+
`doctor` reports which driver is running for each need, **why that one**, and what it
|
|
2402
|
+
costs. The "why" is the point: the registry can fall back from Redis to the filesystem
|
|
2403
|
+
without anyone noticing, and an operator who cannot see that has a site that is slower
|
|
2404
|
+
than they think for a reason nothing told them. Skipped drivers are listed with their
|
|
2405
|
+
reason too.
|
|
2406
|
+
|
|
2407
|
+
It also states out loud what would otherwise be discovered later — that a site with no
|
|
2408
|
+
LLM provider works apart from the agents, that SQLite is one machine with no vector
|
|
2409
|
+
index, and that signed media URLs will not survive a restart without
|
|
2410
|
+
`COGENTA_STORAGE_SIGNING_KEY`. An invalid configuration is reported as the offending
|
|
2411
|
+
fields rather than a stack trace, and exits non-zero so a deployment script notices.
|
|
2412
|
+
|
|
2413
|
+
Core gains `loadConfig` and `findConfigFile`, which walk up from the working directory
|
|
2414
|
+
the way a package manager looks for a lockfile. A missing config file is not an error: a
|
|
2415
|
+
container configured entirely through `COGENTA_*` and `DATABASE_URL` is a legitimate way
|
|
2416
|
+
to run.
|
|
2417
|
+
|
|
2418
|
+
- [`ea82de1`](https://github.com/cogenta-cms/cogenta/commit/ea82de10eba12d520e586b69e1bce733339da26d) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add two error codes for L5's agent format and registry:
|
|
2419
|
+
`AGENT_DEFINITION_INVALID` (`defineAgent` given an empty name or
|
|
2420
|
+
identity document path) and `AGENT_UNKNOWN` (a registry operation named
|
|
2421
|
+
an agent that was never registered).
|
|
2422
|
+
|
|
2423
|
+
- [`8ae3456`](https://github.com/cogenta-cms/cogenta/commit/8ae3456d346ee2e169fceaa45c3cbaef1df01982) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add two error codes for L4's autonomy and approval layer:
|
|
2424
|
+
`TOOL_CALL_REJECTED` (a human rejected a tool call awaiting approval) and
|
|
2425
|
+
`APPROVAL_REQUEST_UNKNOWN` (deciding an approval request id that does not
|
|
2426
|
+
exist).
|
|
2427
|
+
|
|
2428
|
+
- [`0877503`](https://github.com/cogenta-cms/cogenta/commit/0877503bf4a999543d51ce6dda2126471a4852c0) Thanks [@georgesmomo](https://github.com/georgesmomo)! - `create-cogenta` — the `blog` blueprint (L9 task 3): `post`/`category`/`tag`
|
|
2429
|
+
collections, real demo content seeded through `ContentStore`, the canonical
|
|
2430
|
+
theme's default skin (`theme.tokens.json`), and a recommended-agents hint
|
|
2431
|
+
(`.cogenta/recommended-agents.json`) — no live agent scheduler is wired,
|
|
2432
|
+
since none exists anywhere in this codebase yet (R2). `resolveBlueprint`
|
|
2433
|
+
now genuinely resolves `blog` as available; `blank`'s output is unchanged.
|
|
2434
|
+
|
|
2435
|
+
Also fixes a bare `throw new Error(...)` in `resolveBlueprint`'s internal
|
|
2436
|
+
consistency check, replaced with a `CogentaError`.
|
|
2437
|
+
|
|
2438
|
+
One new `@cogenta/core` error code: `BLUEPRINT_REGISTRY_CORRUPT`.
|
|
2439
|
+
|
|
2440
|
+
- [`dc674b2`](https://github.com/cogenta-cms/cogenta/commit/dc674b2dc8a375b8ace5881a3fb8601855888500) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the Postgres and MySQL/MariaDB database drivers.
|
|
2441
|
+
|
|
2442
|
+
Both run the same contract suite as SQLite, against real servers — the L0 exit criterion
|
|
2443
|
+
that the three databases pass one integration suite rather than three that resemble each
|
|
2444
|
+
other.
|
|
2445
|
+
|
|
2446
|
+
`postgres` (postgres.js) and `mysql2` are optional peer dependencies loaded through a
|
|
2447
|
+
dynamic import, so a site on SQLite installs neither and the default install still has no
|
|
2448
|
+
runtime dependency. postgres.js was chosen over `pg` because it has no transitive
|
|
2449
|
+
dependencies at all. Neither package appears in the published type declarations: each
|
|
2450
|
+
driver describes the slice of client API it uses structurally.
|
|
2451
|
+
|
|
2452
|
+
A transaction reserves a single connection for its whole duration. Issuing `BEGIN` on a
|
|
2453
|
+
pool would start the transaction on whichever connection happened to be free and run the
|
|
2454
|
+
following statements on others, silently outside it — a bug that only appears under
|
|
2455
|
+
concurrency, which is where it costs the most. Nested transactions become savepoints on
|
|
2456
|
+
both, matching SQLite.
|
|
2457
|
+
|
|
2458
|
+
`database.poolSize` is configurable and defaults to 5: shared hosting allows very few
|
|
2459
|
+
connections, and exhausting them takes a site down rather than slowing it. MySQL is
|
|
2460
|
+
opened with UTC and `dateStrings`, so a row does not read back differently depending on
|
|
2461
|
+
where the process runs.
|
|
2462
|
+
|
|
2463
|
+
- [`6f0b7bd`](https://github.com/cogenta-cms/cogenta/commit/6f0b7bdd457ba8d81e0aa18d0bde9b583bf810af) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `@cogenta/channels` (L6 task 1): the `ChannelAdapter` interface and
|
|
2464
|
+
`createChannelRegistry`, the foundation for the L6 lot ("Canaux" —
|
|
2465
|
+
Telegram, Slack, Discord, email, webhooks).
|
|
2466
|
+
|
|
2467
|
+
A message is described abstractly — `AlertChannelMessage`,
|
|
2468
|
+
`ReportChannelMessage`, `NotificationChannelMessage` — matching the lot's
|
|
2469
|
+
three fixed formats exactly, so no business code ever writes
|
|
2470
|
+
platform-specific Markdown. `ChannelIdentity.linkedUserId` is `string |
|
|
2471
|
+
null`, representing an unlinked channel identity as a first-class state:
|
|
2472
|
+
the lot's central security rule ("une commande entrante s'exécute avec les
|
|
2473
|
+
permissions de l'humain identifié, jamais avec celles de l'agent")
|
|
2474
|
+
requires that state to exist even before a later task enforces it.
|
|
2475
|
+
`InboundCommand` always carries the `ChannelIdentity` it came from, so a
|
|
2476
|
+
command cannot be routed without knowing who — if anyone — sent it.
|
|
2477
|
+
|
|
2478
|
+
`createChannelRegistry` mirrors `@cogenta/agents`'s `createProviderRegistry`:
|
|
2479
|
+
a site with zero channels configured works fine (R2's spirit), `get()` on
|
|
2480
|
+
an unconfigured name throws a typed `CogentaError` rather than returning
|
|
2481
|
+
`undefined`.
|
|
2482
|
+
|
|
2483
|
+
Two new `@cogenta/core` error codes: `CHANNEL_UNKNOWN`, `CHANNEL_DUPLICATE`.
|
|
2484
|
+
|
|
2485
|
+
- [`fd5ada9`](https://github.com/cogenta-cms/cogenta/commit/fd5ada927327946603a05349c2f87686ef8f003c) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the email channel adapter (L6 task 8) — outbound-only (the lot's task
|
|
2486
|
+
list names this adapter without "commandes entrantes", unlike Telegram's
|
|
2487
|
+
task 4), buttonless: an `Alert`'s two actions render as HMAC-signed,
|
|
2488
|
+
single-use links (`## Approbations depuis le canal`'s explicit guidance for
|
|
2489
|
+
channels without buttons) reusing L6 task 5's real signing primitive rather
|
|
2490
|
+
than a new one.
|
|
2491
|
+
|
|
2492
|
+
R1-compliant `EmailTransport` interface with one real, tested,
|
|
2493
|
+
no-external-service implementation (`createFileEmailTransport`, writes each
|
|
2494
|
+
message to disk) — a real SMTP/HTTP-API transport is a deliberate,
|
|
2495
|
+
documented follow-up, not built in this pass; raw SMTP is a materially
|
|
2496
|
+
larger undertaking than Telegram's plain-HTTP Bot API and was judged
|
|
2497
|
+
disproportionate to this task's scope.
|
|
2498
|
+
|
|
2499
|
+
Two new `@cogenta/core` error codes: `CHANNEL_EMAIL_TRANSPORT_ERROR`,
|
|
2500
|
+
`CHANNEL_EMAIL_INBOUND_UNSUPPORTED`.
|
|
2501
|
+
|
|
2502
|
+
- [`22bb5b2`](https://github.com/cogenta-cms/cogenta/commit/22bb5b2903b35c79d80a7df0bb99bead1533ba55) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `@cogenta/channels`'s identity-linking mechanism (L6 task 2): a
|
|
2503
|
+
one-time code generated on the admin side, verified from a channel, tying
|
|
2504
|
+
a channel-side identity (`channelName`/`channelUserId`) to a real Cogenta
|
|
2505
|
+
user — the piece "## La règle de sécurité centrale" (`docs/lots/L6-canaux.md`)
|
|
2506
|
+
depends on.
|
|
2507
|
+
|
|
2508
|
+
`createChannelLinkStore(db, now?)` — `generateCode`, `verifyCode`,
|
|
2509
|
+
`resolveIdentity`, `revoke`, `listLinkedChannels`, all real, persisted
|
|
2510
|
+
(SQLite/Postgres/MySQL via `ensureChannelTables`, following
|
|
2511
|
+
`@cogenta/auth`'s `ensureAuthTables` pattern — no separate migration file).
|
|
2512
|
+
|
|
2513
|
+
Codes are 8 characters from a 32-symbol unambiguous alphabet (Crockford-style,
|
|
2514
|
+
`0`/`O`/`1`/`I`/`L` removed), 40 bits of entropy, single-use, a short
|
|
2515
|
+
default TTL (10 minutes, "valable quelques minutes" per the lot doc) —
|
|
2516
|
+
judged against brute-forcing one code within its TTL window, not against
|
|
2517
|
+
long-term-secret standards (session tokens remain 256 bits). Stored hashed,
|
|
2518
|
+
never plain, like a session token.
|
|
2519
|
+
|
|
2520
|
+
`verifyCode` rejects every failure kind — nonexistent, expired,
|
|
2521
|
+
already-used, wrong channel — with the same uniform `CHANNEL_LINK_CODE_INVALID`
|
|
2522
|
+
error, so a caller cannot accidentally build a channel-facing reply that
|
|
2523
|
+
leaks which reason applied (an enumeration oracle against unlinked
|
|
2524
|
+
identities).
|
|
2525
|
+
|
|
2526
|
+
One new `@cogenta/core` error code: `CHANNEL_LINK_CODE_INVALID`.
|
|
2527
|
+
|
|
2528
|
+
- [`50d3b40`](https://github.com/cogenta-cms/cogenta/commit/50d3b4041fb5392502711c2bf20f4ec92d2ce76d) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `@cogenta/channels`'s inbound command routing (L6 task 3) — the
|
|
2529
|
+
payoff for tasks 1 and 2: "## La règle de sécurité centrale"
|
|
2530
|
+
(`docs/lots/L6-canaux.md`), **"Une commande entrante s'exécute avec les
|
|
2531
|
+
permissions de l'humain identifié, jamais avec celles de l'agent."**
|
|
2532
|
+
|
|
2533
|
+
`authorizeInboundCommand(identity, requiredRoles, getUserRoles)` — the
|
|
2534
|
+
single security gate every inbound command passes through. An unlinked
|
|
2535
|
+
identity (`linkedUserId: null`) is refused with `shouldReply: false`,
|
|
2536
|
+
matching "Une identité de canal non liée à un compte est ignorée, sans
|
|
2537
|
+
réponse — répondre confirmerait l'existence du bot à un inconnu": a
|
|
2538
|
+
consuming adapter that just checks the flag gets that property for free.
|
|
2539
|
+
A linked-but-unauthorized user is refused with `shouldReply: true`
|
|
2540
|
+
(they're a known person and may be told so). An authorized result always
|
|
2541
|
+
carries the identity's real, verified `linkedUserId` — never anything
|
|
2542
|
+
read off the inbound payload itself.
|
|
2543
|
+
|
|
2544
|
+
`createCommandRouter({getUserRoles})` — parses `/name args`, looks up a
|
|
2545
|
+
registered handler, and routes through `authorizeInboundCommand` before
|
|
2546
|
+
ever invoking it. The unlinked check happens before even checking whether
|
|
2547
|
+
the command is recognized, so an unlinked stranger gets silence for
|
|
2548
|
+
*any* input, not just for commands that exist — an "unknown command"
|
|
2549
|
+
reply would itself leak the bot's existence.
|
|
2550
|
+
|
|
2551
|
+
`requiredRoles` reuses contract A's own open role-name-array convention
|
|
2552
|
+
(`CollectionDefinition.permissions`, `@cogenta/api`'s `PermissionLayer`)
|
|
2553
|
+
rather than a parallel permission-string system.
|
|
2554
|
+
|
|
2555
|
+
One new `@cogenta/core` error code: `CHANNEL_COMMAND_DUPLICATE`.
|
|
2556
|
+
|
|
2557
|
+
- [`27e32b5`](https://github.com/cogenta-cms/cogenta/commit/27e32b52ed11e97969e2b319b2e74345bbc1f213) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add real constructors for the lot's three fixed message levels (L6 task 6)
|
|
2558
|
+
— `buildAlert`/`buildReport`/`buildNotification` (`src/formats/`) — that
|
|
2559
|
+
validate the exact rules `## Formats de message` states in prose: an alert
|
|
2560
|
+
requires a title/context/expected-action and a real admin URL; a report
|
|
2561
|
+
requires at least one key figure and refuses to exceed an abstract
|
|
2562
|
+
480-character screen budget unless a `moreUrl` fallback is given; a
|
|
2563
|
+
notification must be a real, non-empty single line. `approvals/message.ts`
|
|
2564
|
+
now builds its alert through `buildAlert` instead of a hand-assembled
|
|
2565
|
+
literal, so it gets the same validation for free.
|
|
2566
|
+
|
|
2567
|
+
Also hardens the Telegram adapter's report rendering with a real,
|
|
2568
|
+
last-resort truncation at Telegram's actual 4096-character `sendMessage`
|
|
2569
|
+
limit — the `moreUrl` footer is never the part that gets cut, since it's
|
|
2570
|
+
the reader's only way to the full detail.
|
|
2571
|
+
|
|
2572
|
+
New `@cogenta/core` error code: `CHANNEL_MESSAGE_INVALID`.
|
|
2573
|
+
|
|
2574
|
+
- [`962073f`](https://github.com/cogenta-cms/cogenta/commit/962073f3aa5e56e68869c7d14a4b2937e506cfbd) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `@cogenta/channels`'s notification preferences and grouping (L6 task
|
|
2575
|
+
7) — per `(userId, channelName)`: opted-in event types, minimum severity,
|
|
2576
|
+
quiet hours, and a grouping mode (`immediate | hourly | daily`).
|
|
2577
|
+
|
|
2578
|
+
A `NotificationDispatcher.notify()` filters against these preferences and
|
|
2579
|
+
either sends immediately or queues the notification; `flushDue()`
|
|
2580
|
+
collapses every due group into a single message (a `Report` via
|
|
2581
|
+
`buildReport` for more than one queued item, a `Notification` for
|
|
2582
|
+
exactly one) — this is what turns fifteen dependency-scan findings into
|
|
2583
|
+
one grouped message instead of fifteen separate ones
|
|
2584
|
+
("## Préférences", `docs/lots/L6-canaux.md`).
|
|
2585
|
+
|
|
2586
|
+
Quiet hours defer a non-critical notification until the window ends
|
|
2587
|
+
rather than dropping it; a `critical`-severity notification always
|
|
2588
|
+
bypasses quiet hours. Preferences persist via a new `cogenta_channel_preferences`
|
|
2589
|
+
table (`ensurePreferenceTables`), following the same `create table if
|
|
2590
|
+
not exists` pattern as `ensureChannelTables`.
|
|
2591
|
+
|
|
2592
|
+
One new `@cogenta/core` error code: `CHANNEL_PREFERENCES_INVALID`.
|
|
2593
|
+
|
|
2594
|
+
- [`59aced9`](https://github.com/cogenta-cms/cogenta/commit/59aced90e97d3aa2a98ab5e7aa067f50e2ceb611) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the Slack channel adapter (L6 task 9) — the second real
|
|
2595
|
+
`ChannelAdapter`, wired to the same identity linking and inbound command
|
|
2596
|
+
routing Telegram (task 4) already proved.
|
|
2597
|
+
|
|
2598
|
+
Zero-dependency: a small hand-typed client (`createSlackClient`) calls
|
|
2599
|
+
Slack's plain HTTPS/JSON Web API directly via `fetch`, same reasoning as
|
|
2600
|
+
the Telegram client. Socket Mode, not the Events API webhook — Slack's own
|
|
2601
|
+
real, officially-supported answer to "no public HTTPS endpoint," same
|
|
2602
|
+
reasoning as Telegram's long-polling choice (no plane of this project is
|
|
2603
|
+
deployed publicly yet). `createSlackSocketClient` opens a WebSocket via
|
|
2604
|
+
`apps.connections.open`, using Node's built-in `WebSocket` global (stable
|
|
2605
|
+
since this project's Node 22 minimum) — no new dependency needed for the
|
|
2606
|
+
transport either. A `disconnect` envelope triggers exactly one
|
|
2607
|
+
reconnect, mirroring Telegram's continuous poll loop.
|
|
2608
|
+
|
|
2609
|
+
Rate limiting: Slack signals a 429 via an HTTP `Retry-After` header
|
|
2610
|
+
(unlike Telegram's JSON `retry_after` field) — read correctly and
|
|
2611
|
+
retried with the real value, never a guessed backoff.
|
|
2612
|
+
|
|
2613
|
+
Message rendering (`renderSlackMessage`) turns the abstract
|
|
2614
|
+
`ChannelMessage` into Slack Block Kit blocks, entirely inside this
|
|
2615
|
+
adapter. A button's `action_id`/`value` is literally the command text it
|
|
2616
|
+
routes as — a Block Kit button press (`block_actions`) goes through the
|
|
2617
|
+
exact same `CommandRouter.route()` a typed message does, never a second,
|
|
2618
|
+
parallel authorization path. An unlinked identity's message is tried
|
|
2619
|
+
once as a linking code, exactly like Telegram; every other case stays
|
|
2620
|
+
silent.
|
|
2621
|
+
|
|
2622
|
+
Capabilities declared honestly: `threads`/`attachments` are `false` —
|
|
2623
|
+
not built this pass, deferred rather than half-implemented.
|
|
2624
|
+
|
|
2625
|
+
One new `@cogenta/core` error code: `CHANNEL_SLACK_API_ERROR`.
|
|
2626
|
+
|
|
2627
|
+
- [`b26dd9f`](https://github.com/cogenta-cms/cogenta/commit/b26dd9f636095b126ceb78e69bda50f7f5f8cb52) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the Telegram channel adapter (L6 task 4) — "Telegram en premier,
|
|
2628
|
+
complet": the first live `ChannelAdapter`, wired to tasks 2/3's identity
|
|
2629
|
+
linking and inbound command routing.
|
|
2630
|
+
|
|
2631
|
+
Zero-dependency: a small hand-typed client (`createTelegramClient`) calls
|
|
2632
|
+
Telegram's plain HTTPS/JSON Bot API directly via `fetch`, following this
|
|
2633
|
+
project's established precedent (`@cogenta/import`'s WXR parser,
|
|
2634
|
+
`@cogenta/mcp`'s JSON-RPC subset) of a small hand-rolled client over a new
|
|
2635
|
+
SDK dependency for a REST API this simple.
|
|
2636
|
+
|
|
2637
|
+
Transport: long-polling (`getUpdates`), not a webhook — a webhook needs a
|
|
2638
|
+
real public HTTPS endpoint and Telegram's own signature verification, and
|
|
2639
|
+
no plane of this project is deployed publicly yet (L9 task 12's scoping).
|
|
2640
|
+
Polling works unchanged wherever `cogenta serve` already runs.
|
|
2641
|
+
|
|
2642
|
+
Rate limiting: a 429 response is retried using Telegram's own
|
|
2643
|
+
`retry_after` value, never a guessed backoff — "Prévoir la file, le
|
|
2644
|
+
backoff et le regroupement dès le premier adaptateur."
|
|
2645
|
+
|
|
2646
|
+
Message rendering (`renderTelegramMessage`) turns the abstract
|
|
2647
|
+
`ChannelMessage` (alert/report/notification) into MarkdownV2 text plus
|
|
2648
|
+
inline keyboard buttons, entirely inside this adapter — "on n'écrit pas
|
|
2649
|
+
de Markdown Telegram dans le code métier." A button's `callback_data` is
|
|
2650
|
+
literally the command text it routes as: a button press goes through the
|
|
2651
|
+
exact same `CommandRouter.route()` a typed command does, never a second,
|
|
2652
|
+
parallel authorization path. An unlinked identity's message is tried
|
|
2653
|
+
once as a linking code (confirmed on success, silent on any failure) —
|
|
2654
|
+
"Une identité de canal non liée à un compte est ignorée, sans réponse"
|
|
2655
|
+
still holds for everything else.
|
|
2656
|
+
|
|
2657
|
+
One new `@cogenta/core` error code: `CHANNEL_TELEGRAM_API_ERROR`.
|
|
2658
|
+
|
|
2659
|
+
- [`f52f97f`](https://github.com/cogenta-cms/cogenta/commit/f52f97ff8c553ab44f715b55f37ac726ea335160) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the generic signed webhook channel (L6 task 11, the final task of the
|
|
2660
|
+
L6 lot) — the security primitive "## Pièges connus" names explicitly:
|
|
2661
|
+
"Les webhooks entrants sont une surface d'attaque. Vérification de
|
|
2662
|
+
signature obligatoire, fenêtre temporelle, protection contre le rejeu."
|
|
2663
|
+
|
|
2664
|
+
`verifyIncomingWebhook` checks all three, mandatory: HMAC-SHA256
|
|
2665
|
+
signature authenticity (constant-time comparison, same construction as
|
|
2666
|
+
`approvals/signed-link.ts`), timestamp freshness against an injectable
|
|
2667
|
+
clock (default 5-minute window), and replay rejection via a bounded
|
|
2668
|
+
in-memory `WebhookReplayGuard` — a request failing any check is rejected
|
|
2669
|
+
with a distinguishable, typed `CogentaError` (unlike identity-linking's
|
|
2670
|
+
deliberately uniform code, there is no enumeration oracle here: a
|
|
2671
|
+
webhook secret is either configured correctly or it isn't, and
|
|
2672
|
+
distinguishing "bad signature" from "stale timestamp" from "already
|
|
2673
|
+
processed" is a legitimate operator need). `signOutgoingWebhook` is the
|
|
2674
|
+
matching outbound half `createWebhookAdapter`'s `send()` uses on every
|
|
2675
|
+
request — round-trip tested against the real verifier, not just each
|
|
2676
|
+
half in isolation.
|
|
2677
|
+
|
|
2678
|
+
`createWebhookAdapter` is outbound-only: `capabilities.buttons` is
|
|
2679
|
+
`false` (no UI to click — actions render as real signed links, reusing
|
|
2680
|
+
the same primitive the email adapter, task 8, already consumes) and
|
|
2681
|
+
`capabilities.inbound` is `false` — real inbound command execution for
|
|
2682
|
+
an arbitrary third-party caller is a materially larger undertaking
|
|
2683
|
+
(a live HTTP route, per-integration identity decisions) than this
|
|
2684
|
+
task's actual deliverable, the signing/verification primitive itself,
|
|
2685
|
+
which is complete and exercised end-to-end. `verifyIdentity` is an
|
|
2686
|
+
honest refusal, matching the email adapter's precedent.
|
|
2687
|
+
|
|
2688
|
+
Three new `@cogenta/core` error codes: `CHANNEL_WEBHOOK_SIGNATURE_INVALID`,
|
|
2689
|
+
`CHANNEL_WEBHOOK_EXPIRED`, `CHANNEL_WEBHOOK_REPLAY_DETECTED`, plus
|
|
2690
|
+
`CHANNEL_WEBHOOK_DELIVERY_FAILED` and `CHANNEL_WEBHOOK_INBOUND_UNSUPPORTED`.
|
|
2691
|
+
|
|
2692
|
+
L6 ("Canaux") is now complete — all 11 tasks done.
|
|
2693
|
+
|
|
2694
|
+
- [`39b6d33`](https://github.com/cogenta-cms/cogenta/commit/39b6d339a52ba97a1437167c15910971eee02383) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the database layer: the dialect abstraction, the SQLite driver and their contract
|
|
2695
|
+
suite.
|
|
2696
|
+
|
|
2697
|
+
Callers write `` sql`… ${value}` `` and never a placeholder: Postgres wants `$1` where
|
|
2698
|
+
MySQL and SQLite want `?`, and letting that reach a call site is the dialect leak the
|
|
2699
|
+
design warns about. The same layer quotes identifiers per dialect and adapts values —
|
|
2700
|
+
SQLite has no boolean and no date type, MySQL's `datetime` carries no time zone — so a
|
|
2701
|
+
caller never has to know which database is connected. Interpolated values are always
|
|
2702
|
+
bound; only `unsafeRaw`, named to say so, inserts text verbatim.
|
|
2703
|
+
|
|
2704
|
+
The SQLite driver uses Node's built-in `node:sqlite`, so the default install compiles
|
|
2705
|
+
nothing and depends on nothing. `better-sqlite3` is deliberately avoided: it is native
|
|
2706
|
+
code, and rule R10 forbids that without a fallback because it breaks on ARM, musl and
|
|
2707
|
+
shared hosting — the deployments SQLite exists to serve. WAL mode, a busy timeout and
|
|
2708
|
+
foreign keys are on from the first connection, and nested transactions map onto
|
|
2709
|
+
savepoints so two functions that each want a transaction compose.
|
|
2710
|
+
|
|
2711
|
+
- [`6322731`](https://github.com/cogenta-cms/cogenta/commit/632273109648e850e415bb179bea6e5ea027c500) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the `HTTP_FETCH_DOMAIN_DENIED` error code, thrown by `@cogenta/agents`'
|
|
2712
|
+
`http.fetch` core tool when a requested URL's host is not on the calling
|
|
2713
|
+
agent's allowed domain list.
|
|
2714
|
+
|
|
2715
|
+
- [`4921407`](https://github.com/cogenta-cms/cogenta/commit/4921407b4dbd283bdd76cf74d288a79c2ebcab64) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `@cogenta/auth` — passwords, TOTP, WebAuthn passkeys, opaque sessions, progressive
|
|
2716
|
+
login rate-limiting, and a hash-chained audit log, tested against a real SQLite database
|
|
2717
|
+
(no mocked database, per AGENTS.md).
|
|
2718
|
+
|
|
2719
|
+
Passwords use `scrypt` from `node:crypto` at the OWASP floor (N=2^15), never bcrypt or
|
|
2720
|
+
argon2 — both are native modules R10 forbids without a WASM fallback, and neither ships
|
|
2721
|
+
one. TOTP (RFC 6238) is hand-written, forty lines of unambiguous HMAC; WebAuthn is a
|
|
2722
|
+
justified dependency (`@simplewebauthn/server`, MIT, pure JS) because attestation
|
|
2723
|
+
verification is a large, security-relevant surface no homegrown subset should touch.
|
|
2724
|
+
|
|
2725
|
+
MFA is mandatory, not configurable, for the `admin` role and for any role a collection
|
|
2726
|
+
grants `publish` to — computed from `CollectionDefinition[]`, so it tracks the schema
|
|
2727
|
+
rather than a setting someone can switch off under deadline pressure. A short-lived
|
|
2728
|
+
HMAC-signed ticket (the same shape as a preview grant) carries a verified password step
|
|
2729
|
+
into the second-factor step without server-side state.
|
|
2730
|
+
|
|
2731
|
+
Sessions are opaque random bearer tokens, stored hashed like a password, sliding TTL —
|
|
2732
|
+
never a JWT, so "sign out of every device" is a real revoke rather than a wait for
|
|
2733
|
+
expiry. The audit log is append-only and hash-chained; `verify()` detects a row edited or
|
|
2734
|
+
deleted outside of `record()`, and this table is built to take a second writer once L4's
|
|
2735
|
+
agents need to log to the same place.
|
|
2736
|
+
|
|
2737
|
+
`newId`/`isUuidV7`/`timestampOf` move from `@cogenta/schema` to `@cogenta/core`, since
|
|
2738
|
+
`@cogenta/auth` now needs them too; `@cogenta/schema` re-exports them unchanged.
|
|
2739
|
+
|
|
2740
|
+
- [`7d9ed38`](https://github.com/cogenta-cms/cogenta/commit/7d9ed3878de61d54e58a4aa027c72447c118761c) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the `database` job queue driver — the degraded tier that needs no Redis and no
|
|
2741
|
+
persistent worker.
|
|
2742
|
+
|
|
2743
|
+
Jobs are claimed with `FOR UPDATE SKIP LOCKED` on Postgres, MySQL and MariaDB, and under
|
|
2744
|
+
the write lock on SQLite. Two different mechanisms, one guarantee, proven by one contract
|
|
2745
|
+
suite: L0's acceptance criterion is that two concurrent workers never process the same
|
|
2746
|
+
job, and it is asserted with real connections racing on a real database rather than a
|
|
2747
|
+
mock. A claim that loses an InnoDB deadlock retries, because both MySQL and Postgres
|
|
2748
|
+
document that as the remedy rather than a failure.
|
|
2749
|
+
|
|
2750
|
+
A worker only claims jobs it has a handler for, so two workers with different handlers
|
|
2751
|
+
take their own work instead of locking jobs they would have to put back. A job whose
|
|
2752
|
+
worker dies is released when its lease expires. Failures retry with exponential backoff
|
|
2753
|
+
and stop at `maxAttempts`, recording why.
|
|
2754
|
+
|
|
2755
|
+
Two dialect traps are now handled in the db layer rather than by callers: `LIMIT` renders
|
|
2756
|
+
as a literal, because MySQL prepared statements reject a placeholder there, and the SQLite
|
|
2757
|
+
driver serialises statements per file within a process — `node:sqlite` is synchronous, so
|
|
2758
|
+
a second connection issuing a write while the first holds a transaction deadlocks the
|
|
2759
|
+
event loop rather than waiting.
|
|
2760
|
+
|
|
2761
|
+
- [`046ffa8`](https://github.com/cogenta-cms/cogenta/commit/046ffa85769066150a0d0e8443d0d257ef72239c) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add one error code for L5's evaluation harness: `EVAL_THRESHOLD_NOT_MET`
|
|
2762
|
+
(`assertEvalThreshold`'s suite mean score fell below the required
|
|
2763
|
+
minimum — the mechanism a `*.eval.test.ts` file uses to fail CI on a
|
|
2764
|
+
prompt or model regression).
|
|
2765
|
+
|
|
2766
|
+
- [`a958ee1`](https://github.com/cogenta-cms/cogenta/commit/a958ee12cee1130effb97e95d58fda219e153a4c) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `@cogenta/api`: the permission layer, preview tokens, REST and GraphQL.
|
|
2767
|
+
|
|
2768
|
+
Both transports run on one permission layer, as the lot requires. The hardest rule —
|
|
2769
|
+
the `public` role never reaches a draft, on any route, in either transport, whatever the
|
|
2770
|
+
query says — is enforced structurally rather than by condition: `canReadUnpublished`
|
|
2771
|
+
strips `public` from the actor's roles before looking at anything, so even a collection
|
|
2772
|
+
misconfigured with `update: ['public']` cannot become draft access.
|
|
2773
|
+
|
|
2774
|
+
A preview token is the single deliberate exception, and it is scoped to one entry. That
|
|
2775
|
+
scoping is not free: `canReadUnpublished` is only told which collection is being read, so
|
|
2776
|
+
a grant for entry A would otherwise unlock every draft in it. Every path that returns
|
|
2777
|
+
entries filters each one through `previewCovers` — the list, the paginated connection,
|
|
2778
|
+
reads by id, and relation expansion including the batching loader.
|
|
2779
|
+
|
|
2780
|
+
REST is a router over normalised request and response objects, with no HTTP framework and
|
|
2781
|
+
no listening socket, so it is tested without a server. Filters use a fixed vocabulary and
|
|
2782
|
+
values are coerced from the declared field kind, because a text comparison would rank
|
|
2783
|
+
`"10"` below `"9"`. GraphQL derives its schema from the collections, prints the same
|
|
2784
|
+
object it executes, and batches relation reads through a thirty-line dataloader written
|
|
2785
|
+
here rather than taken as a dependency.
|
|
2786
|
+
|
|
2787
|
+
- [`39fc7a4`](https://github.com/cogenta-cms/cogenta/commit/39fc7a4d490f0a1683ef69dd5495e0ff6494ca72) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add typed errors and configuration loading.
|
|
2788
|
+
|
|
2789
|
+
`CogentaError` carries a stable `code`, an actionable `hint` and opt-in structured
|
|
2790
|
+
`details`. It is the only error library code throws — a bare `throw new Error("…")`
|
|
2791
|
+
gives callers nothing to branch on and users nothing to act on.
|
|
2792
|
+
|
|
2793
|
+
`defineConfig` types `cogenta.config.ts`; `resolveConfig` validates it and applies
|
|
2794
|
+
defaults, then environment overrides. Secrets (`llm.apiKey`, `storage.accessKeyId`,
|
|
2795
|
+
`storage.secretAccessKey`) are rejected in the config file and read from the
|
|
2796
|
+
environment only. Unknown keys are errors rather than silently ignored settings, an
|
|
2797
|
+
invalid configuration reports every offending field at once, and the database driver
|
|
2798
|
+
is inferred from the URL scheme when it is not named.
|
|
2799
|
+
|
|
2800
|
+
- [`1c5efd2`](https://github.com/cogenta-cms/cogenta/commit/1c5efd24572d6295e5e21f476637adf8ebc92819) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the skin system of contract D to `@cogenta/render`: design tokens, CSS variables and
|
|
2801
|
+
hot swap.
|
|
2802
|
+
|
|
2803
|
+
**Tokens.** `validateSkin` takes a raw `tokens.json` and returns it typed, or refuses it.
|
|
2804
|
+
The token set is closed: a missing token *and* an unknown token are both refused, so a
|
|
2805
|
+
skin can never leave a variable undefined nor smuggle in presentation the theme never
|
|
2806
|
+
declared.
|
|
2807
|
+
|
|
2808
|
+
**Validation, in hard-refusal mode.** WCAG 2.2 AA contrast on the three declared pairs
|
|
2809
|
+
(`fg`/`bg`, `accentFg`/`accent`, `mutedFg`/`muted`), a strictly increasing typographic
|
|
2810
|
+
ladder, well-formed colours, lengths and durations, and `motion.reduced`. A refusal names
|
|
2811
|
+
every failure of its category at once — for contrast, the pair, its measured ratio and the
|
|
2812
|
+
shortfall. Relative luminance and the contrast ratio are computed in the package, without
|
|
2813
|
+
a dependency.
|
|
2814
|
+
|
|
2815
|
+
**CSS.** `renderSkinCss` emits one stylesheet of `--cogenta-<group>-<name>` custom
|
|
2816
|
+
properties, plus the derived font-size ladder and a density multiplier, and honours
|
|
2817
|
+
`prefers-reduced-motion` in the sheet itself rather than only in the token.
|
|
2818
|
+
|
|
2819
|
+
**Hot swap.** `createSkinStore(tokens).apply(next)` validates and rewrites the sheet with
|
|
2820
|
+
no build step, in well under a millisecond, and keeps the previous skin live if the new
|
|
2821
|
+
one is refused. Each sheet carries a content ETag that is stable for identical tokens.
|
|
2822
|
+
|
|
2823
|
+
New error codes in `@cogenta/core`: `SKIN_TOKEN_MISSING`, `SKIN_TOKEN_UNKNOWN`,
|
|
2824
|
+
`SKIN_TOKEN_INVALID`, `SKIN_CONTRAST_INSUFFICIENT`, `SKIN_SCALE_NOT_MONOTONIC`,
|
|
2825
|
+
`SKIN_MOTION_NOT_REDUCED`.
|
|
2826
|
+
|
|
2827
|
+
- [`2a044a1`](https://github.com/cogenta-cms/cogenta/commit/2a044a1689f98a25258b6f45d9baf0b325194c95) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the `bullmq` job queue driver — the optimal tier, on Redis — as the counterpart to the
|
|
2828
|
+
`database` driver. Both now run **one** contract suite, so a site that loses its Redis
|
|
2829
|
+
falls back without a line of calling code changing.
|
|
2830
|
+
|
|
2831
|
+
`bullmq` stays an optional peer: it is loaded by dynamic import through `loadBullmqModule()`,
|
|
2832
|
+
which returns `null` when it is absent, and the API it exposes is described structurally so
|
|
2833
|
+
the published type declarations never reference it. A site on the database queue installs
|
|
2834
|
+
neither `bullmq` nor `ioredis`, and still typechecks.
|
|
2835
|
+
|
|
2836
|
+
Jobs are fetched by hand rather than by a `Worker` loop, because `tick()` is the call both
|
|
2837
|
+
drivers answer to — cron drives it on shared hosting, and it has to mean the same thing on
|
|
2838
|
+
Redis. The atomic claim stays inside Redis, so the L0 acceptance criterion holds: four
|
|
2839
|
+
workers draining twenty-four jobs never process one twice. A job whose worker was killed is
|
|
2840
|
+
returned to the queue by bullmq's stalled checker, which manual fetching does not start on
|
|
2841
|
+
its own — the driver starts it.
|
|
2842
|
+
|
|
2843
|
+
Two mappings are worth knowing. Cogenta priorities run high-first and bullmq's run low-first
|
|
2844
|
+
with `0` reserved, so priorities are mirrored onto a mid-range origin; and bullmq has no
|
|
2845
|
+
cancelled state, so a cancelled job is removed and recorded in a tombstone hash under the
|
|
2846
|
+
driver's own key prefix. Job ids carry the job name, because bullmq shards by queue.
|
|
2847
|
+
|
|
2848
|
+
`available()` opens a connection and pings rather than trusting that a URL is configured, and
|
|
2849
|
+
`health()` never reports the URL — it routinely carries a password.
|
|
2850
|
+
|
|
2851
|
+
- [`5ae4e24`](https://github.com/cogenta-cms/cogenta/commit/5ae4e24e59cf807ef7aca5839623fd8a24798435) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the MCP client: `createMcpStdioClient` spawns a third-party MCP
|
|
2852
|
+
server as a child process and speaks the same stdio JSON-RPC protocol
|
|
2853
|
+
as the server side (task 17). `wrapMcpTool` turns a remote tool into an
|
|
2854
|
+
ordinary `ToolDefinition` — permissions, `sideEffects`, `reversible` and
|
|
2855
|
+
`cost` are declared by the integrator, never trusted from the remote
|
|
2856
|
+
server, so a wrapped remote tool passes through the exact same registry,
|
|
2857
|
+
manifest, audit and autonomy pipeline as an internal one.
|
|
2858
|
+
|
|
2859
|
+
Two new `@cogenta/core` error codes: `MCP_CLIENT_REMOTE_ERROR` (the
|
|
2860
|
+
remote server answered with a JSON-RPC protocol error) and
|
|
2861
|
+
`MCP_CLIENT_TOOL_FAILED` (the remote tool itself reported `isError:
|
|
2862
|
+
true`).
|
|
2863
|
+
|
|
2864
|
+
- [`77ff957`](https://github.com/cogenta-cms/cogenta/commit/77ff95771e3fc415d9581e8d51ccae200167703d) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add one error code for L4's memory layer:
|
|
2865
|
+
`AGENT_APPROVAL_NOT_DECIDED` (converting a still-pending approval request
|
|
2866
|
+
into a memory record — only a decided one carries a learning signal).
|
|
2867
|
+
|
|
2868
|
+
- [`3021aa1`](https://github.com/cogenta-cms/cogenta/commit/3021aa1c65d708b1267c662ce925d560f735d7d0) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the structured logger.
|
|
2869
|
+
|
|
2870
|
+
`createLogger` emits one JSON object per line — never free text — with a level, an ISO
|
|
2871
|
+
timestamp, a message and the caller's fields merged at the top level. Fields cannot
|
|
2872
|
+
overwrite the record structure. `child()` binds context that repeats on every record
|
|
2873
|
+
without touching its parent.
|
|
2874
|
+
|
|
2875
|
+
Every record passes through redaction on the way out: by field name (`apiKey`,
|
|
2876
|
+
`secretAccessKey`, `authorization`), by value shape (provider key prefixes, private key
|
|
2877
|
+
blocks, JWTs) and inside connection strings, where only the password is replaced so the
|
|
2878
|
+
URL stays readable for debugging. Fields that merely look related — `tokens`,
|
|
2879
|
+
`tokensPerDay`, `cacheKey` — are left alone, because over-redacting makes logs useless.
|
|
2880
|
+
|
|
2881
|
+
`Error` values are unpacked explicitly rather than left to `JSON.stringify`, which
|
|
2882
|
+
renders them as `{}`, and an unserialisable field drops the field rather than throwing
|
|
2883
|
+
in the caller's face.
|
|
2884
|
+
|
|
2885
|
+
- [`b2ecf93`](https://github.com/cogenta-cms/cogenta/commit/b2ecf9310366fcbaf18fbbf2c71bc45fccc577da) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Wire Drizzle onto the database layer, on all three dialects.
|
|
2886
|
+
|
|
2887
|
+
Every dialect goes through Drizzle's **proxy** driver rather than the driver Drizzle
|
|
2888
|
+
ships for it, so ORM traffic runs on the same connection as raw SQL — one pool, the same
|
|
2889
|
+
transaction pinning, the same typed errors, and the same rule that a parameter value
|
|
2890
|
+
never reaches an error message. On SQLite there was no choice anyway: `better-sqlite3` is
|
|
2891
|
+
forbidden by rule R10 and `node:sqlite` has no Drizzle driver.
|
|
2892
|
+
|
|
2893
|
+
`SqlExecutor` grows three things the bridge needs and nothing else has to use: `dialect`
|
|
2894
|
+
moves down from `DatabaseHandle` so a transaction executor knows what it is talking to,
|
|
2895
|
+
`execute()` runs SQL that is already rendered for the dialect without encoding its values
|
|
2896
|
+
a second time, and `asArrays` returns rows as ordered values — a join selecting
|
|
2897
|
+
`users.id` and `posts.id` loses one of them in an object keyed by column name.
|
|
2898
|
+
|
|
2899
|
+
`db.transaction()` on a proxy instance is not usable. Use `drizzleTransaction`, which
|
|
2900
|
+
runs the work inside a handle transaction and rebuilds the instance on its executor, so
|
|
2901
|
+
every statement lands on the pinned connection and rolls back with it.
|
|
2902
|
+
|
|
2903
|
+
- [`8d3b27c`](https://github.com/cogenta-cms/cogenta/commit/8d3b27ce2334c7ea6e75182707aa6d6e78688b31) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the three build targets — static, Node SSR, edge — and the refusal that keeps a static
|
|
2904
|
+
build honest.
|
|
2905
|
+
|
|
2906
|
+
The target is a build parameter, never a theme variant: the renderer is handed the route
|
|
2907
|
+
and nothing else, so it *cannot* branch on the target. That is what makes "the same
|
|
2908
|
+
content produces an equivalent result on the three targets" a property of the code rather
|
|
2909
|
+
than a promise, and the equivalence test compares the three builds byte for byte.
|
|
2910
|
+
|
|
2911
|
+
A build collects every declared runtime need first — blocks, theme, plugins — and judges
|
|
2912
|
+
afterwards. `collectionList` is the one block of the twelve that declares
|
|
2913
|
+
`runtime: 'server'`, so the case is real on the first site that places a listing. On a
|
|
2914
|
+
static target the build is refused before a single page is rendered, with a message that
|
|
2915
|
+
names the block, the pages it sits on and their block keys, why a static build cannot
|
|
2916
|
+
carry it, and the three ways out: build for `--target node` or `--target edge`, move the
|
|
2917
|
+
work to an external service the browser calls, or remove the element. Every offending
|
|
2918
|
+
element is reported at once, so an operator takes one decision rather than one build per
|
|
2919
|
+
problem. Nothing degrades silently: no dropped block, no build-time snapshot of a live
|
|
2920
|
+
list.
|
|
2921
|
+
|
|
2922
|
+
On the two request-time targets the split is hybrid and identical: pages with a
|
|
2923
|
+
server-side need are served on request, the rest are still prerendered. An unregistered
|
|
2924
|
+
block is refused rather than assumed static — guessing a runtime is how a server-side
|
|
2925
|
+
block slips into a static build.
|
|
2926
|
+
|
|
2927
|
+
Core adds two error codes, `BUILD_TARGET_UNKNOWN` and `BUILD_RUNTIME_UNSATISFIED`.
|
|
2928
|
+
|
|
2929
|
+
- [`ee839be`](https://github.com/cogenta-cms/cogenta/commit/ee839be0c862bea209acd080b6a44bcd41738d5a) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the core of `@cogenta/schema`: `defineCollection`, the fourteen field types of
|
|
2930
|
+
contract A, the system fields, and the two generated artefacts.
|
|
2931
|
+
|
|
2932
|
+
`f.text()`, `f.richText()`, `f.slug()`, `f.number()`, `f.boolean()`, `f.date()`,
|
|
2933
|
+
`f.datetime()`, `f.media()`, `f.relation()`, `f.select()`, `f.json()`, `f.geo()`,
|
|
2934
|
+
`f.color()` and `f.blocks()` each produce a plain, serialisable field definition and a
|
|
2935
|
+
Zod validator derived from it — one validator, generated from the schema, never a second
|
|
2936
|
+
one written by hand next to it.
|
|
2937
|
+
|
|
2938
|
+
`defineCollection` checks a definition at import time and reports **every** problem at
|
|
2939
|
+
once, each located by the field it concerns (`fields.author.onDelete`,
|
|
2940
|
+
`indexes[0]`, `routing.pattern`), rather than one per run. A default value the field
|
|
2941
|
+
itself would reject, a slug derived from a field nobody declared, `'setNull'` on a
|
|
2942
|
+
required relation, an action outside the five of the contract: all refused before a
|
|
2943
|
+
migration exists.
|
|
2944
|
+
|
|
2945
|
+
`renderTypeDeclarations()` produces `.cogenta/types.d.ts` — one interface per collection,
|
|
2946
|
+
extending the system fields, importing nothing so a theme compiles against it without
|
|
2947
|
+
depending on the schema package. A theme reading a field that no longer exists now fails
|
|
2948
|
+
to build, which is the acceptance criterion of L1. `renderSchemaJson()` produces
|
|
2949
|
+
`.cogenta/schema.json`, the description the admin reads. Both are pure functions
|
|
2950
|
+
returning strings; the CLI writes the files.
|
|
2951
|
+
|
|
2952
|
+
`richText` stores the restricted Portable Text document of ADR-0013 — no HTML, no `h1`,
|
|
2953
|
+
internal links referencing an entity rather than a URL — and rejects a mark that no
|
|
2954
|
+
`markDefs` entry defines or two nodes sharing a `_key`. Ids are application-minted
|
|
2955
|
+
UUIDv7 (ADR-0015), monotonic inside a millisecond so they stay ordered.
|
|
2956
|
+
|
|
2957
|
+
Core gains the `SCHEMA_INVALID` error code.
|
|
2958
|
+
|
|
2959
|
+
- [`8d3b27c`](https://github.com/cogenta-cms/cogenta/commit/8d3b27ce2334c7ea6e75182707aa6d6e78688b31) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the image pipeline, the three build targets, the tag-invalidated page cache and the
|
|
2960
|
+
PWA to `@cogenta/render`.
|
|
2961
|
+
|
|
2962
|
+
Images are a driver like any other: `sharp` at the optimal tier as an optional peer, a
|
|
2963
|
+
WebAssembly libvips fallback at the degraded tier, and one contract suite run against
|
|
2964
|
+
both. The fallback runs **unconditionally**, not when `sharp` happens to be missing — a
|
|
2965
|
+
suite that stops exercising it on the maintainer's laptop is exactly the hole L3 warns
|
|
2966
|
+
about. `/_image` caps requested dimensions, because it is a public URL and a loop over
|
|
2967
|
+
widths would otherwise be a cache-filling attack.
|
|
2968
|
+
|
|
2969
|
+
A build target is a parameter, never a theme variant: the renderer is handed a route and
|
|
2970
|
+
returns a string, so a theme cannot branch on the target even if it wanted to, and
|
|
2971
|
+
equivalence across static, Node and edge is structural rather than promised. A static
|
|
2972
|
+
build carrying a `runtime: 'server'` block is refused with a message naming the element,
|
|
2973
|
+
where it sits in the site, and three numbered ways out — asserted byte for byte so the
|
|
2974
|
+
wording cannot quietly degrade.
|
|
2975
|
+
|
|
2976
|
+
The page cache derives its tags by instrumenting what a render actually read, not by
|
|
2977
|
+
declaration, which would be wrong at the first omission. A list page carries its
|
|
2978
|
+
collection's tag and a detail page does not, so publishing an entry that was never in
|
|
2979
|
+
the cached page still drops the list.
|
|
2980
|
+
|
|
2981
|
+
- [`40539fc`](https://github.com/cogenta-cms/cogenta/commit/40539fcd48da958dba69f9a32f0b440f868d539f) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the Performance agent: `queryCrux` measures Core Web Vitals via the
|
|
2982
|
+
Chrome UX Report API (real-user field data on the deployed site, no
|
|
2983
|
+
headless browser); `medianMetrics` combines several noisy samples
|
|
2984
|
+
before `compareToBudget` or `detectRegression` ever run
|
|
2985
|
+
(`detectRegression`'s default 15% threshold is deliberately generous,
|
|
2986
|
+
so normal field-data jitter never gets reported as a regression);
|
|
2987
|
+
`diagnosePerformanceRisks` flags only structurally-derivable causes
|
|
2988
|
+
(missing image dimensions, unoptimized images, too many third-party
|
|
2989
|
+
scripts) — it does not guess at causes it cannot back with data.
|
|
2990
|
+
`performanceAgent` ties it together with the lot's tool list
|
|
2991
|
+
(`http.fetch`/`content.read`/`channel.send`/`build.trigger` — no
|
|
2992
|
+
content-writing tools).
|
|
2993
|
+
|
|
2994
|
+
One new `@cogenta/core` error code: `PERFORMANCE_CRUX_QUERY_FAILED`.
|
|
2995
|
+
|
|
2996
|
+
- [`24b1745`](https://github.com/cogenta-cms/cogenta/commit/24b174536c79a7b0f505e1ba4e70d5070fb14f6d) Thanks [@georgesmomo](https://github.com/georgesmomo)! - L9 task 12 ("Site du projet et playground"), the buildable slice the lot itself calls out: "commencer par une démo en lecture seule réinitialisée périodiquement."
|
|
2997
|
+
|
|
2998
|
+
- `@cogenta/schema`: new `withReadOnlyStore(store)` — wraps any `ContentStore` so `create`/`update`/`delete`/`publish`/`unpublish`/`restore` refuse with a real `CONTENT_READ_ONLY` error while every read passes through unchanged.
|
|
2999
|
+
- `@cogenta/cli`: `runServe`'s `ServeOptions` gained a `readOnly` flag. Wrapped once, at the single point `serve.ts` constructs every `ContentStore` — both REST's `ContentService` and GraphQL's gateway share it, so neither transport can bypass the guard.
|
|
3000
|
+
- `@cogenta/api`: `CONTENT_READ_ONLY` maps to HTTP 403.
|
|
3001
|
+
- `@cogenta/core`: two new error codes — `CONTENT_READ_ONLY`, `PLAYGROUND_BLUEPRINT_UNKNOWN`.
|
|
3002
|
+
- `create-cogenta`: new `resetPlaygroundData()` — wipes and reseeds a blueprint's tables back to its own real demo content (`BLUEPRINT_CONTENT_PACKS`, unchanged, not a second parallel demo dataset). A real, tested, callable unit; scheduling it periodically is an operational decision for whoever deploys a read-only instance, not made here. `BLUEPRINT_CONTENT_PACKS`/`BlueprintContentPack` are now part of the package's public exports.
|
|
3003
|
+
|
|
3004
|
+
Actual public deployment of a playground or the project site is explicitly out of scope: it is an irreversible action toward the outside world requiring resources only a human holds, per this project's standing autonomy rule.
|
|
3005
|
+
|
|
3006
|
+
Also new: `@cogenta/project-site` (private, unpublished) — a small, real presentation site for the Cogenta project itself, built through the same content model and `renderPage`/`renderBlock` pipeline any installed site uses, with real content drawn from `docs/00-vision.md` and this session's own documentation.
|
|
3007
|
+
|
|
3008
|
+
- [`3184163`](https://github.com/cogenta-cms/cogenta/commit/318416355a83d88828786344e1ff80e1b113c564) Thanks [@georgesmomo](https://github.com/georgesmomo)! - `runPlugin` (L7 task 6) now enforces the lot's own words in full: "un plugin
|
|
3009
|
+
qui dépasse son temps ou sa mémoire est tué et désactivé, avec alerte. Il ne
|
|
3010
|
+
peut pas faire tomber le CMS."
|
|
3011
|
+
|
|
3012
|
+
- A worker failure is now classified (`IsolatedRunResult.reason`:
|
|
3013
|
+
`'timeout' | 'memory' | 'crash'`) — `'memory'` is detected from Node's
|
|
3014
|
+
real `resourceLimits` heap-violation error message, `'timeout'` from the
|
|
3015
|
+
existing kill switch, everything else is `'crash'`.
|
|
3016
|
+
- Only a `'timeout'` or `'memory'` violation disables the plugin — an
|
|
3017
|
+
ordinary thrown error never does. Disablement is real and persisted
|
|
3018
|
+
(`createPluginDisableStore`, `cogenta_plugin_disabled` table, mirroring
|
|
3019
|
+
`cogenta_plugin_grants`'s `ensurePluginTables` pattern). `runPlugin` now
|
|
3020
|
+
requires a `disableStore` and refuses (`PLUGIN_DISABLED`, a new
|
|
3021
|
+
`@cogenta/core` error code) to even spawn a worker for an already-disabled
|
|
3022
|
+
plugin — checked before every run, not just after a violation.
|
|
3023
|
+
- The "avec alerte" half is a structural callback (`onPluginDisabled`), not
|
|
3024
|
+
a hard dependency on `@cogenta/channels` or any specific transport —
|
|
3025
|
+
wiring a disablement to a real notification is an integration decision
|
|
3026
|
+
for whatever assembles a site.
|
|
3027
|
+
- Proven by real, worker-based tests: a genuine heap-exhaustion fixture
|
|
3028
|
+
trips the real `resourceLimits` ceiling and is classified `'memory'`; the
|
|
3029
|
+
host process is proven to survive and remain usable (a follow-up run
|
|
3030
|
+
succeeds immediately after either violation type); a disabled plugin's
|
|
3031
|
+
next run attempt is refused before a worker is spawned; a human can
|
|
3032
|
+
re-enable a disabled plugin.
|
|
3033
|
+
|
|
3034
|
+
- [`1f2eecc`](https://github.com/cogenta-cms/cogenta/commit/1f2eecc754286c9e140511634b465a6536f99f25) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Adds L7 task 9: real signature verification for registry-sourced plugins,
|
|
3035
|
+
per "## Signature" (docs/lots/L7-extensibilite.md): "Une signature invalide
|
|
3036
|
+
bloque, sans possibilité de passer outre depuis l'interface."
|
|
3037
|
+
|
|
3038
|
+
- `packages/plugins/src/signing/` — real Ed25519 signing/verification via
|
|
3039
|
+
`node:crypto` (no new dependency): `generateSigningKeyPair`, `signManifest`
|
|
3040
|
+
(signs a deterministic, sorted-key canonicalization of the manifest),
|
|
3041
|
+
`verifyManifestSignature`/`verifyPluginSignature` (verifies against any
|
|
3042
|
+
of a list of trusted public keys), `readSignatureFile` (a signature travels
|
|
3043
|
+
as a sibling `<manifest>.sig` file, never embedded in the manifest shape).
|
|
3044
|
+
- `TRUSTED_REGISTRY_PUBLIC_KEYS` starts empty — no real plugin registry
|
|
3045
|
+
exists yet (pre-alpha), so every `registry`-source plugin fails
|
|
3046
|
+
verification by default rather than trusting a placeholder key.
|
|
3047
|
+
- `loadPlugin` (L7 task 2) now calls `resolveSignatureStatus` for every
|
|
3048
|
+
resolution: a `registry`-source plugin with a missing or invalid signature
|
|
3049
|
+
is hard-refused (`PLUGIN_SIGNATURE_MISSING`/`PLUGIN_SIGNATURE_INVALID`)
|
|
3050
|
+
before any plugin code is imported — there is no parameter anywhere that
|
|
3051
|
+
lets a caller force past this. A `local`/`git`-source plugin is allowed
|
|
3052
|
+
unsigned ("mode développement") and now carries a real `devMode: true`
|
|
3053
|
+
flag on `ResolvedPlugin` (plus `signatureVerified: boolean`) for a future
|
|
3054
|
+
admin banner to render as the lot's "avertissement permanent."
|
|
3055
|
+
|
|
3056
|
+
Two new `@cogenta/core` error codes: `PLUGIN_SIGNATURE_MISSING`,
|
|
3057
|
+
`PLUGIN_SIGNATURE_INVALID`.
|
|
3058
|
+
|
|
3059
|
+
- [`6ce944f`](https://github.com/cogenta-cms/cogenta/commit/6ce944ffac8e947a979b8dc46a64ee3699b0b402) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add one error code for L4's privacy layer: `PRIVACY_NO_DATA_LEAVES_VIOLATION`
|
|
3060
|
+
(a run configured with `privacyPolicy.enabled: true` tried to call a
|
|
3061
|
+
provider outside its declared local allowlist).
|
|
3062
|
+
|
|
3063
|
+
- [`fc13c44`](https://github.com/cogenta-cms/cogenta/commit/fc13c4484c1c01a64b23941622e8308731fd937e) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add four error codes for L4's LLM provider adapters: `PROVIDER_UNKNOWN`,
|
|
3064
|
+
`PROVIDER_REQUEST_FAILED`, `PROVIDER_RESPONSE_INVALID`, `PROVIDER_RATE_LIMITED`.
|
|
3065
|
+
|
|
3066
|
+
- [`f5b0d4c`](https://github.com/cogenta-cms/cogenta/commit/f5b0d4cd8b7a81b36f8c539b38a412b893cb125c) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the `PROVIDER_TIMEOUT` error code, thrown by `@cogenta/agents`' execution
|
|
3067
|
+
loop when a model call does not respond within its configured `timeoutMs`.
|
|
3068
|
+
|
|
3069
|
+
- [`1041c9f`](https://github.com/cogenta-cms/cogenta/commit/1041c9fb8c39872350786e5dc5b8a4f84e2b3ff7) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `MediaStore` to `@cogenta/core` — the persisted metadata record for a media
|
|
3070
|
+
asset (alt text, decorative flag with a required justification, focal point,
|
|
3071
|
+
dimensions, storage key), backed by one SQL table played against SQLite,
|
|
3072
|
+
Postgres and MySQL through the same contract, the same shape as the degraded
|
|
3073
|
+
job queue. Nothing wired this to a route yet: L2 task 11 (médiathèque) is
|
|
3074
|
+
still in progress, and this is its data layer.
|
|
3075
|
+
|
|
3076
|
+
Alt text policy is enforced in the store, not left to a caller to remember:
|
|
3077
|
+
a non-decorative asset needs non-empty alt text, and a decorative one needs a
|
|
3078
|
+
justification, writing `alt=""` regardless of what was passed — matching
|
|
3079
|
+
L2-admin.md's own rule that a decorative image never gets an invented
|
|
3080
|
+
description.
|
|
3081
|
+
|
|
3082
|
+
`sniffImageFormat`/`describeContainer` (real-type detection by magic bytes,
|
|
3083
|
+
never by filename or `Content-Type`) moved from `@cogenta/render` into
|
|
3084
|
+
`@cogenta/core`, since the upcoming media upload route needs the exact same
|
|
3085
|
+
check and depending on `@cogenta/render` for four byte-signature functions
|
|
3086
|
+
would pull in its Astro/sharp integration for no reason. `@cogenta/render`
|
|
3087
|
+
re-exports both from its own `images` module, so no call site there changes.
|
|
3088
|
+
|
|
3089
|
+
ADR-0017 records the SVG policy this data layer assumes: refused by default,
|
|
3090
|
+
never served raw, until a reviewed sanitizer exists.
|
|
3091
|
+
|
|
3092
|
+
- [`ed7e7d1`](https://github.com/cogenta-cms/cogenta/commit/ed7e7d1cd73eedff8877c974938b7134bd24ac3b) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the migration engine: tracking table, apply, rollback and status.
|
|
3093
|
+
|
|
3094
|
+
`down()` is a required method, not an optional one. AGENTS.md says migrations are always
|
|
3095
|
+
reversible, and a type that permits an irreversible migration turns that rule into a
|
|
3096
|
+
suggestion.
|
|
3097
|
+
|
|
3098
|
+
A destructive migration refuses to run without **both** an explicit confirmation and a
|
|
3099
|
+
verified backup, and the refusal names what each one will do to existing data so the
|
|
3100
|
+
confirmation is informed rather than reflexive.
|
|
3101
|
+
|
|
3102
|
+
An applied migration that changed is refused rather than re-run or ignored: two
|
|
3103
|
+
environments that ran different SQL under the same id differ in ways nothing records.
|
|
3104
|
+
`status()` reports the mismatch without throwing, so a diagnosis can still run.
|
|
3105
|
+
|
|
3106
|
+
Migrations take an exclusive lock, so two deployments cannot migrate at once — the
|
|
3107
|
+
primary key does the work, and a lock left by a crashed process is taken over after
|
|
3108
|
+
fifteen minutes. Each migration runs in a transaction where the database has
|
|
3109
|
+
transactional DDL. **MySQL does not**, so a failed migration there may be half applied;
|
|
3110
|
+
the engine says so in the error instead of claiming a rollback that never happened.
|
|
3111
|
+
|
|
3112
|
+
- [`fe1e7b6`](https://github.com/cogenta-cms/cogenta/commit/fe1e7b693d3a5eb8635e783a75863f5613712fb4) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the storage driver interface, the `local` implementation and their contract suite.
|
|
3113
|
+
|
|
3114
|
+
Object keys are validated against a whitelist of allowed characters per segment rather
|
|
3115
|
+
than a blacklist of dangerous ones — keys arrive from uploads, imports and plugins, and
|
|
3116
|
+
a blacklist loses to URL encoding, backslashes and Unicode look-alikes. Every operation
|
|
3117
|
+
validates, so a traversal attempt raises instead of quietly reporting "not found".
|
|
3118
|
+
|
|
3119
|
+
The `local` driver keeps objects and their metadata in two parallel trees. Storing the
|
|
3120
|
+
metadata next to the object would make it addressable as an object itself: readable
|
|
3121
|
+
under a guessable key, overwritable through a forged one, and colliding with any key
|
|
3122
|
+
that happened to end in the sidecar suffix.
|
|
3123
|
+
|
|
3124
|
+
Signed URLs are HMAC-signed and verified in constant time. Without
|
|
3125
|
+
`COGENTA_STORAGE_SIGNING_KEY` the driver generates a per-process key and says so through
|
|
3126
|
+
`health()`, rather than silently issuing URLs that stop working after a restart.
|
|
3127
|
+
|
|
3128
|
+
`StorageDriver` also gains `head()`: the content type is supplied by the caller and
|
|
3129
|
+
cannot be recovered later, so an interface with no way to read it back would lose it.
|
|
3130
|
+
|
|
3131
|
+
- [`a609efa`](https://github.com/cogenta-cms/cogenta/commit/a609efa46060a35b048a24e7d03b7bbde414b7a4) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add three error codes for L4's reversibility layer:
|
|
3132
|
+
`RECEIPT_UNKNOWN` (reverting a receipt id that does not exist),
|
|
3133
|
+
`RECEIPT_ALREADY_REVERTED` (reverting a receipt a second time), and
|
|
3134
|
+
`RECEIPT_NOT_REVERTIBLE` (the matching tool has no `revert()` available in
|
|
3135
|
+
the current run).
|
|
3136
|
+
|
|
3137
|
+
- [`32dc81a`](https://github.com/cogenta-cms/cogenta/commit/32dc81adac441ecc0b105c4da02e9064ead09b99) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the S3 storage driver as the optimal tier, verified against MinIO.
|
|
3138
|
+
|
|
3139
|
+
The AWS SDK is an optional peer loaded through a dynamic import: a site storing media on
|
|
3140
|
+
disk installs none of it, and the published type declarations do not reference it. A
|
|
3141
|
+
buffer goes through `PutObject`; a stream goes through multipart `Upload`, so a large
|
|
3142
|
+
video is never buffered in memory to be stored.
|
|
3143
|
+
|
|
3144
|
+
`forcePathStyle` is set whenever a custom endpoint is configured. MinIO, R2 and most
|
|
3145
|
+
self-hosted gateways serve buckets as a path rather than a subdomain, and assuming
|
|
3146
|
+
virtual-host style breaks all of them with what looks like a DNS error.
|
|
3147
|
+
|
|
3148
|
+
The contract suite also caught a parity break: an object stored with no declared content
|
|
3149
|
+
type reads back as `application/octet-stream` from S3 but was `undefined` from the local
|
|
3150
|
+
driver, which would have made the two impossible to substitute when serving media. Both
|
|
3151
|
+
now return the HTTP default.
|
|
3152
|
+
|
|
3153
|
+
- [`f0915d5`](https://github.com/cogenta-cms/cogenta/commit/f0915d5b3040512560477cfbb95729a6e69a3f3c) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the `deps.scan` tool: SBOM → OSV.dev correlation (only versions
|
|
3154
|
+
genuinely installed and affected, matched by OSV's own query semantics)
|
|
3155
|
+
→ EPSS lookup → exploitability assessment crossing CVSS and EPSS →
|
|
3156
|
+
imposed-format security report (what's affected / what an attacker
|
|
3157
|
+
could do / is the site exposed / what's proposed / what happens if
|
|
3158
|
+
nothing is done).
|
|
3159
|
+
|
|
3160
|
+
Two new `@cogenta/core` error codes: `SECURITY_OSV_QUERY_FAILED` and
|
|
3161
|
+
`SECURITY_EPSS_QUERY_FAILED`.
|
|
3162
|
+
|
|
3163
|
+
- [`269c38b`](https://github.com/cogenta-cms/cogenta/commit/269c38b4df5bae381cadbfa85d5c6fe12353e177) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `deps.patch` (opens a pull request bumping one dependency to a fixed
|
|
3164
|
+
version — never modifies anything directly; `revert` closes the PR
|
|
3165
|
+
without merging) and `securityAgent`, the frozen `AgentDeclaration`
|
|
3166
|
+
tying `deps.scan`/`deps.patch` together with the lot's default autonomy
|
|
3167
|
+
(`deps.scan` autonomous, `deps.patch` proposed).
|
|
3168
|
+
|
|
3169
|
+
One new `@cogenta/core` error code: `SECURITY_DEPENDENCY_NOT_FOUND`.
|
|
3170
|
+
|
|
3171
|
+
- [`ee839be`](https://github.com/cogenta-cms/cogenta/commit/ee839be0c862bea209acd080b6a44bcd41738d5a) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add slugs, routing, automatic 301 redirects and scheduled publication to `@cogenta/schema`.
|
|
3172
|
+
|
|
3173
|
+
**Slugs.** `slugify` transliterates with `normalize('NFD')` and a written-down table for
|
|
3174
|
+
the letters Unicode does not decompose — "ß", "æ", "ø" — so it needs no dependency and no
|
|
3175
|
+
data file. `deriveSlug` reads the source named by `f.slug({ from: 'title' })`, keeps a
|
|
3176
|
+
slug the editor typed by hand, and resolves collisions with a `-2`, `-3` suffix that
|
|
3177
|
+
stays inside the length budget rather than growing past the column width. Uniqueness is
|
|
3178
|
+
scoped **per collection and per locale**, which is what ADR-0014 implies: the French and
|
|
3179
|
+
the English article are two entries, and both are legitimately `/mon-article` under their
|
|
3180
|
+
own prefix.
|
|
3181
|
+
|
|
3182
|
+
**Redirects.** Changing the slug of a **published** entry now writes a 301 with nobody
|
|
3183
|
+
asking for it, and the table is listable, filterable and deletable. Two properties are
|
|
3184
|
+
enforced at write time rather than left to whoever reads the table later:
|
|
3185
|
+
|
|
3186
|
+
- chains are flattened — renaming a page twice leaves one hop, not two, so a visitor
|
|
3187
|
+
never pays for the site's edit history;
|
|
3188
|
+
- loops are refused with `CONTENT_REDIRECT_LOOP`, and moving a page back to its old URL
|
|
3189
|
+
is expressed as `release()` rather than as a cycle the store quietly repairs.
|
|
3190
|
+
|
|
3191
|
+
A draft that changes slug records nothing: nobody could reach the old URL, and a redirect
|
|
3192
|
+
from an unreachable path is a row that only ever confuses.
|
|
3193
|
+
|
|
3194
|
+
**Routing.** `matchPath` resolves a URL against `routing.pattern`, with or without the
|
|
3195
|
+
locale prefix, and `buildPath` goes the other way. `resolveUrl` answers `entry`,
|
|
3196
|
+
`redirect` or `notFound` — content first, redirects second, so a stale rule can never
|
|
3197
|
+
shadow a page that is live.
|
|
3198
|
+
|
|
3199
|
+
**Scheduled publication.** An entry in `status: 'scheduled'` becomes a job in the L0
|
|
3200
|
+
queue, and the whole module is written against `QueueDriver` and nothing else. It
|
|
3201
|
+
therefore works on the `database` queue — the driver with no worker of its own, drained
|
|
3202
|
+
by a cron calling `tick()`. On a cron every five minutes, a page scheduled for 09:00 goes
|
|
3203
|
+
live between 09:00 and 09:05; that is the honest promise of a host without a worker, and
|
|
3204
|
+
the handler logs the lateness so the question can be answered when it is asked. An entry
|
|
3205
|
+
whose hour passed while the site was down publishes on the next tick instead of being
|
|
3206
|
+
skipped.
|
|
3207
|
+
|
|
3208
|
+
`@cogenta/core` gains five error codes for the above: `CONTENT_SLUG_INVALID`,
|
|
3209
|
+
`CONTENT_SLUG_TAKEN`, `CONTENT_ROUTE_INVALID`, `CONTENT_REDIRECT_LOOP` and
|
|
3210
|
+
`CONTENT_SCHEDULE_INVALID`. Adding a code is a minor change; no existing code changed
|
|
3211
|
+
meaning.
|
|
3212
|
+
|
|
3213
|
+
- [`ee839be`](https://github.com/cogenta-cms/cogenta/commit/ee839be0c862bea209acd080b6a44bcd41738d5a) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the content persistence layer: typed CRUD, drafts, versions, diff and i18n, portable
|
|
3214
|
+
across Postgres, MySQL, MariaDB and SQLite.
|
|
3215
|
+
|
|
3216
|
+
`createContentStore({ db, collection })` gives a collection its create/read/update/
|
|
3217
|
+
delete/list, plus `publish`, `unpublish`, `history`, `readVersion`, `restore`, `diff`,
|
|
3218
|
+
`translations` and `resolveLocale`. `createSchemaTables(db, collections)` builds the
|
|
3219
|
+
physical schema the store expects — the same DDL the migration generator will emit, so
|
|
3220
|
+
the two cannot drift.
|
|
3221
|
+
|
|
3222
|
+
The entry table holds the **live** state, which is what the public renderer reads. With
|
|
3223
|
+
`versioning.drafts`, editing a published entry writes a version row and leaves the live
|
|
3224
|
+
row alone: a draft is unreachable through `read(id)` because it is not there, not
|
|
3225
|
+
because a filter remembered to exclude it. Publishing moves the live row onto the
|
|
3226
|
+
working version. `versioning.keep` bounds the history, and the live version is never
|
|
3227
|
+
pruned.
|
|
3228
|
+
|
|
3229
|
+
Pagination is by keyset cursor, never by offset: a cursor is the sort value and the id of
|
|
3230
|
+
the last row handed out, so entries inserted concurrently cannot shift a window and make
|
|
3231
|
+
a reader see the same entry twice or miss one. A cursor taken under one ordering is
|
|
3232
|
+
refused under another.
|
|
3233
|
+
|
|
3234
|
+
Identifiers are UUIDv7 minted by the application (ADR-0015) — no `RETURNING`, no
|
|
3235
|
+
`insertId`, and content keeps its identity across dev, staging and production. Blocks are
|
|
3236
|
+
one row each, ordered, with a stable `_key` (contract A), so "which pages use this
|
|
3237
|
+
medium", cache-tag invalidation and per-block RAG chunking stay possible. Content is one
|
|
3238
|
+
entry per language (ADR-0014): `status`, `publishedAt` and `version` are per language,
|
|
3239
|
+
and a missing locale renders through one of three explicit strategies — show the
|
|
3240
|
+
original, hide it, or report it missing.
|
|
3241
|
+
|
|
3242
|
+
Core gains three error codes — `CONTENT_NOT_FOUND`, `CONTENT_INVALID` and
|
|
3243
|
+
`CONTENT_CONFLICT` — so the content layer reports what failed with a code callers can
|
|
3244
|
+
branch on, instead of borrowing `CONFIG_INVALID` for an editor's mistake.
|
|
3245
|
+
|
|
3246
|
+
- [`11d592b`](https://github.com/cogenta-cms/cogenta/commit/11d592bbca9cea415c95aa0edb4a85aef8b05174) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add two error codes for L4's skills layer:
|
|
3247
|
+
`SKILL_UNKNOWN` (loading a skill name that was never installed) and
|
|
3248
|
+
`SKILL_DEFINITION_INVALID` (a `SKILL.md` has no frontmatter block, or is
|
|
3249
|
+
missing `name`/`version`/`description`).
|
|
3250
|
+
|
|
3251
|
+
- [`64b43fb`](https://github.com/cogenta-cms/cogenta/commit/64b43fb661784c855c1375dfcf995999198e93d3) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the driver system: interface, registry, selection and health reporting.
|
|
3252
|
+
|
|
3253
|
+
`createDriverRegistry` holds the implementations of one infrastructure need and picks
|
|
3254
|
+
between them by two different rules. When the configuration **names** a driver, that
|
|
3255
|
+
driver is used and any failure is fatal — starting on the filesystem because Redis was
|
|
3256
|
+
down, and saying nothing, would be a silent downgrade of someone's site. When it names
|
|
3257
|
+
nothing (or `auto`), the first available driver wins in tier order, and failures fall
|
|
3258
|
+
through to the next one, so `npm create cogenta` produces a working site with nothing
|
|
3259
|
+
else installed.
|
|
3260
|
+
|
|
3261
|
+
Every selection carries a `reason` and the list of what was `skipped` and why, so the
|
|
3262
|
+
admin and `cogenta doctor` can state "job queue: database (degraded), because Redis is
|
|
3263
|
+
absent". A driver whose availability probe throws counts as absent rather than crashing
|
|
3264
|
+
startup, and `dispose()` is idempotent because shutdown paths overlap.
|
|
3265
|
+
|
|
3266
|
+
- [`c93a5f7`](https://github.com/cogenta-cms/cogenta/commit/c93a5f709bce8b380c270a1b4ef31dac86293535) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `auth.signingKey` to the resolved configuration, read from
|
|
3267
|
+
`COGENTA_AUTH_SIGNING_KEY` — the key `@cogenta/auth`'s login ticket needs, and a real
|
|
3268
|
+
secret rather than a config-file field (rule R7): there is no `auth` section in the input
|
|
3269
|
+
schema at all, so writing one in `cogenta.config.ts` is rejected as an unrecognised key,
|
|
3270
|
+
not merely a forbidden one.
|
|
3271
|
+
|
|
3272
|
+
- [`1c5efd2`](https://github.com/cogenta-cms/cogenta/commit/1c5efd24572d6295e5e21f476637adf8ebc92819) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the rendering layer: `@cogenta/render`, `@cogenta/theme-canonical` and `@cogenta/seo`.
|
|
3273
|
+
|
|
3274
|
+
A theme reads content through an HTTP client carrying a read-only token, never through
|
|
3275
|
+
the data layer (ADR-0016), and the isolation is checked at install rather than documented
|
|
3276
|
+
and hoped for. A hostile-theme fixture proves the refusal against every route in: a bare
|
|
3277
|
+
`fs` alias, a subpath import, a template-literal dynamic import, `createRequire`, an
|
|
3278
|
+
import inside a `<script>`, and a `node:fs` alias smuggled through `package.json`
|
|
3279
|
+
`imports`. The inverse guard matters as much — a theme whose prose contains `don't`, a
|
|
3280
|
+
class named `process` and a commented-out import yields zero findings.
|
|
3281
|
+
|
|
3282
|
+
The canonical theme implements the twelve blocks with no JavaScript at all, asserted:
|
|
3283
|
+
no script tag, no `on*` attribute, no `client:*` directive. Heading levels are read from
|
|
3284
|
+
the block vocabulary rather than restated, so a titleless `featureGrid` keeps its items
|
|
3285
|
+
at `h2` and no level is skipped. `consentRequired` suppresses even the provider
|
|
3286
|
+
thumbnail, because a thumbnail already leaks the visitor's IP.
|
|
3287
|
+
|
|
3288
|
+
Skins validate as hard refusals: AA contrast on every declared pair with no epsilon on
|
|
3289
|
+
the threshold, a monotonic type scale, no missing and no unknown token, and
|
|
3290
|
+
`prefers-reduced-motion` honoured. A token value containing CSS syntax is refused — a
|
|
3291
|
+
skin is a shareable JSON file interpolated into a stylesheet, and without that check it
|
|
3292
|
+
is code rather than data.
|
|
3293
|
+
|
|
3294
|
+
SEO derives JSON-LD from the schema, keeps `hreflang` reciprocal by construction, and
|
|
3295
|
+
blocks indexing on the working state as well as on draft status: a feed rendered from
|
|
3296
|
+
the working face ships unreviewed edits, which is the same leak as a draft and far
|
|
3297
|
+
harder to notice.
|
|
3298
|
+
|
|
3299
|
+
- [`163d88b`](https://github.com/cogenta-cms/cogenta/commit/163d88bc594b457a06e19ce39e4fbe9e4693e4d8) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add two error codes for L4's sub-agent layer:
|
|
3300
|
+
`AGENT_SUBAGENT_UNKNOWN` (a declared sub-agent name is not in the agent
|
|
3301
|
+
set) and `AGENT_SUBAGENT_TOOLS_NOT_SUBSET` (a sub-agent declares a tool
|
|
3302
|
+
its parent was not granted).
|
|
3303
|
+
|
|
3304
|
+
- [`73acd6f`](https://github.com/cogenta-cms/cogenta/commit/73acd6f40a6c1904fde717891f04079d930a0e43) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the cache drivers: `memory` and `file`, plus their single contract suite.
|
|
3305
|
+
|
|
3306
|
+
`invalidateTags` is mandatory in every implementation, servers or not — content caching
|
|
3307
|
+
is only correct if publishing can drop every page that embedded the changed content, and
|
|
3308
|
+
bolting that on later would mean rewriting each driver.
|
|
3309
|
+
|
|
3310
|
+
Values round-trip through serialisation in `memory` too, not just on disk, so a caller
|
|
3311
|
+
cannot mutate the cache by keeping the reference on one driver and not on another. Both
|
|
3312
|
+
drivers run the same contract file, which is what makes them substitutable rather than
|
|
3313
|
+
merely similar.
|
|
3314
|
+
|
|
3315
|
+
The `file` driver hashes keys into filenames rather than escaping them, writes through a
|
|
3316
|
+
uniquely named temporary file and an atomic rename, and retries the rename on the EPERM
|
|
3317
|
+
and EBUSY that Windows returns when another handle holds the target. A corrupted entry
|
|
3318
|
+
reads as a miss: a cache that throws is worse than a cache that forgets.
|
|
3319
|
+
|
|
3320
|
+
- [`1c5efd2`](https://github.com/cogenta-cms/cogenta/commit/1c5efd24572d6295e5e21f476637adf8ebc92819) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `@cogenta/render`: the Astro integration, the `RenderContext`, and the theme installation check.
|
|
3321
|
+
|
|
3322
|
+
- `cogentaTheme()` is an Astro integration that resolves the active theme from the
|
|
3323
|
+
configuration, aliases its sources as `@theme`, and exposes its manifest through the
|
|
3324
|
+
virtual module `virtual:cogenta/theme`. The content token never enters Vite's module
|
|
3325
|
+
graph.
|
|
3326
|
+
- `createRenderContext()` builds exactly the `RenderContext` contract D freezes at
|
|
3327
|
+
`theme@1.0` — `site`, `locale`, `url`, `t()`, `image()`, `link()`, `content` — and
|
|
3328
|
+
nothing else.
|
|
3329
|
+
- `ctx.content` is an HTTP client to the content API carrying a read-only token
|
|
3330
|
+
(ADR-0016). It links against neither `@cogenta/schema` nor a database driver, so a
|
|
3331
|
+
theme cannot reach a draft even by asking.
|
|
3332
|
+
- `verifyTheme()` refuses a theme at installation, naming file, line and import, when it
|
|
3333
|
+
does not declare the twelve blocks of the vocabulary, or when it reaches for a
|
|
3334
|
+
forbidden module — directly, through the unprefixed spelling of a builtin, through a
|
|
3335
|
+
subpath, through an unreadable dynamic `import()`, through CommonJS, or through a
|
|
3336
|
+
`package.json` alias. Refused, not warned.
|
|
3337
|
+
|
|
3338
|
+
`@cogenta/core` gains the error codes `THEME_NOT_FOUND`, `THEME_INVALID`,
|
|
3339
|
+
`THEME_BLOCK_MISSING`, `THEME_IMPORT_FORBIDDEN` and `CONTENT_API_FAILED`.
|
|
3340
|
+
|
|
3341
|
+
- [`696c163`](https://github.com/cogenta-cms/cogenta/commit/696c163c05bb981413e52af74d63dcbcbe72c99e) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add five error codes for L4's tool registry: `TOOL_DEFINITION_INVALID`,
|
|
3342
|
+
`TOOL_DUPLICATE`, `TOOL_UNKNOWN`, `TOOL_INPUT_INVALID`, `TOOL_OUTPUT_INVALID`.
|
|
3343
|
+
|
|
3344
|
+
- [`d5896bb`](https://github.com/cogenta-cms/cogenta/commit/d5896bb8bbabb43873e82deb1acfdb818def201b) Thanks [@georgesmomo](https://github.com/georgesmomo)! - New package `@cogenta/import`: `cogenta import wordpress <file.xml>` (L9 task
|
|
3345
|
+
6). Imports a WordPress "Export All Content" WXR file — posts, pages,
|
|
3346
|
+
categories, tags, media (downloaded and re-stored through `MediaStore`/
|
|
3347
|
+
`StorageDriver`), authors (as real, credential-less users), approved comments,
|
|
3348
|
+
postmeta (carried as opaque `f.json()` `customFields`, contract A has no
|
|
3349
|
+
free-form field kind), Gutenberg blocks converted to the block vocabulary
|
|
3350
|
+
(`prose`/`mediaFigure`/`quote`/`gallery`/`embed`) where a mapping exists, and
|
|
3351
|
+
301 redirects from each entry's old permalink (`reason: 'import'`, the
|
|
3352
|
+
`@cogenta/schema` redirect store's own case for this). Every WXR reader is a
|
|
3353
|
+
zero-dependency, WXR-scoped XML tokenizer (`deps-auditor` rejected
|
|
3354
|
+
`fast-xml-parser`: a single-maintainer seven-package split published the same
|
|
3355
|
+
day, and a general parser's DTD support is an unnecessary XXE surface for a
|
|
3356
|
+
file of unknown provenance) — a document declaring `<!DOCTYPE ... ENTITY` is
|
|
3357
|
+
rejected outright.
|
|
3358
|
+
|
|
3359
|
+
Nothing that cannot be converted is silently dropped: an unmappable Gutenberg
|
|
3360
|
+
block, a dead media URL, an author with no email, a trashed post — every one
|
|
3361
|
+
of them lands in the returned `ConversionReport` (`imported`/`skipped`/
|
|
3362
|
+
`unconvertedBlocks`/`warnings`), which `cogenta import wordpress` prints. The
|
|
3363
|
+
command exits `0` even with items reported as unconverted — a reported
|
|
3364
|
+
partial import is the intended outcome for a real-world export, not a
|
|
3365
|
+
failure — and only exits non-zero when the file cannot be read or parsed at
|
|
3366
|
+
all.
|
|
3367
|
+
|
|
3368
|
+
Two new `@cogenta/core` error codes: `IMPORT_WXR_PARSE_FAILED`,
|
|
3369
|
+
`IMPORT_WXR_UNSAFE_DOCUMENT`.
|
|
3370
|
+
|
|
3371
|
+
- [`693697e`](https://github.com/cogenta-cms/cogenta/commit/693697ed41174c027c5acaa43abb3a9c0e41bbab) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add the Redis cache driver, as the optimal tier.
|
|
3372
|
+
|
|
3373
|
+
`@redis/client` is an **optional peer dependency**, loaded through a dynamic import. A
|
|
3374
|
+
site that does not want Redis never installs it, `pnpm install` stays free of runtime
|
|
3375
|
+
dependencies, and the registry simply falls through to the file driver when the package
|
|
3376
|
+
or the server is absent. The published type declarations do not reference it either: the
|
|
3377
|
+
driver describes the slice of the client API it uses structurally.
|
|
3378
|
+
|
|
3379
|
+
Keys are namespaced, and `clear()` walks them with `SCAN` rather than `FLUSHDB` — the
|
|
3380
|
+
Redis instance may be serving other things, and a cache driver that wipes someone else's
|
|
3381
|
+
data is an incident, not a clear. Expiry is written as `PX` so Redis can reclaim memory
|
|
3382
|
+
on its own clock, but the authoritative check stays on read, which is what keeps this
|
|
3383
|
+
driver's behaviour identical to the others.
|
|
3384
|
+
|
|
3385
|
+
- [`ee839be`](https://github.com/cogenta-cms/cogenta/commit/ee839be0c862bea209acd080b6a44bcd41738d5a) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `@cogenta/blocks`: the twelve-block semantic vocabulary of contract B.
|
|
3386
|
+
|
|
3387
|
+
`defineBlock` declares a block manifest — `name`, `version`, `schema`, `runtime`,
|
|
3388
|
+
`fallback`, `a11y` — and compiles it into a Zod validator. The twelve blocks of
|
|
3389
|
+
`blocks@1.0` ship registered and ready: `hero`, `prose`, `mediaFigure`,
|
|
3390
|
+
`featureGrid`, `cta`, `gallery`, `quote`, `faq`, `stats`, `logos`,
|
|
3391
|
+
`collectionList` and `embed`.
|
|
3392
|
+
|
|
3393
|
+
- `parseBlock` / `parseBlocks` validate on write and refuse anything
|
|
3394
|
+
presentational: HTML in a text field, an unrecognised `className`, a `style`
|
|
3395
|
+
value. The error names the block and the field.
|
|
3396
|
+
- `loadBlock` / `loadBlocks` migrate a block whose schema version has moved on,
|
|
3397
|
+
one version step at a time, and report `migrated` so the caller writes the
|
|
3398
|
+
result back. A block's `_key` survives the migration by construction.
|
|
3399
|
+
- Register your own steps on a `BlockMigrationRegistry`; a missing step is a
|
|
3400
|
+
refusal, never a silent partial migration.
|
|
3401
|
+
|
|
3402
|
+
`@cogenta/core` gains the `BLOCK_UNKNOWN`, `BLOCK_INVALID`,
|
|
3403
|
+
`BLOCK_DEFINITION_INVALID` and `BLOCK_MIGRATION_FAILED` error codes.
|
|
3404
|
+
|
|
3405
|
+
### Patch Changes
|
|
3406
|
+
|
|
3407
|
+
- [`b18a02c`](https://github.com/cogenta-cms/cogenta/commit/b18a02c3f5638520794db83bd1adfdb246a4f839) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Add `cogenta migrate` — `status`, `up` and `down` — over the existing migration engine.
|
|
3408
|
+
|
|
3409
|
+
`status` lists every migration with the date and duration of its run, and marks the ones
|
|
3410
|
+
that changed after they were applied here. That last case exits non-zero: two
|
|
3411
|
+
environments that ran different SQL under the same id is the worst state to debug, and a
|
|
3412
|
+
deployment script has to notice it rather than read it.
|
|
3413
|
+
|
|
3414
|
+
Migrations are plain ESM files in a `migrations/` directory next to the configuration
|
|
3415
|
+
file, default-exporting an object with `up(tx)` and `down(tx)`. They are ordered by file
|
|
3416
|
+
name, the id defaults to the file name, and the checksum is a hash of the file itself —
|
|
3417
|
+
so a migration edited after it ran is detected without anyone maintaining a second
|
|
3418
|
+
number. A project with no `migrations/` directory has zero migrations, which is not an
|
|
3419
|
+
error: L0 ships no business schema at all.
|
|
3420
|
+
|
|
3421
|
+
A destructive migration still needs `--confirm-destructive` **and** `--backup-verified`.
|
|
3422
|
+
The engine already refused without both; the CLI now makes the refusal actionable by
|
|
3423
|
+
naming each destructive migration and printing its declared impact, instead of asking
|
|
3424
|
+
the operator to go and read the files.
|
|
3425
|
+
|
|
3426
|
+
Core fix, found by running the command from a subdirectory: a relative path in a config
|
|
3427
|
+
file is now resolved against **that file**, not against the shell's working directory.
|
|
3428
|
+
`cogenta migrate status` run from `src/` used to open an empty `./site.db` next to `src/`
|
|
3429
|
+
and report an already-migrated database as entirely pending. The same applies to
|
|
3430
|
+
`cache.path` and `storage.path`. Absolute paths, server URLs and `:memory:` are
|
|
3431
|
+
untouched, and configuration that comes from the environment alone is unaffected.
|
|
3432
|
+
|
|
3433
|
+
- [`f870177`](https://github.com/cogenta-cms/cogenta/commit/f8701772440a4b3a7d0726b0836b94b7c1b57344) Thanks [@georgesmomo](https://github.com/georgesmomo)! - Adds a complete Discord channel adapter (L6 task 10): outbound messages
|
|
3434
|
+
rendered as real embeds with button components, inbound message/interaction
|
|
3435
|
+
handling routed through the existing identity-linking and human-permission
|
|
3436
|
+
authorization gate (never a parallel path), a real Gateway WebSocket client
|
|
3437
|
+
with deterministic heartbeat scheduling, and 429 rate-limit handling using
|
|
3438
|
+
Discord's actual `retry_after` value. One new `@cogenta/core` error code:
|
|
3439
|
+
`CHANNEL_DISCORD_API_ERROR`.
|
|
3440
|
+
|
|
3441
|
+
- [`d10724c`](https://github.com/cogenta-cms/cogenta/commit/d10724cb238399bf7203fff0bc151a832c555ad4) Thanks [@georgesmomo](https://github.com/georgesmomo)! - `@cogenta/fleet` gains site-side telemetry emission (`packages/fleet/src/agent/`) — the closed, honest shape of what a site is allowed to send to the control plane, per the lot's own "## Ce qui remonte, et ce qui ne remonte pas."
|
|
3442
|
+
|
|
3443
|
+
- `TelemetryPayload` is a closed type: only the fields the lot doc names
|
|
3444
|
+
(`installedVersions`, `sbomFingerprint`, `openCves`, `coreWebVitalsAggregate`,
|
|
3445
|
+
`availability`, `backups`, `certificateExpiry`, `adminAccounts`,
|
|
3446
|
+
`aggregatedErrors`) exist on it — no `content`/`media`/`memory`/`logs`
|
|
3447
|
+
field is representable at all. `sbomFingerprint`, `openCves`,
|
|
3448
|
+
`coreWebVitalsAggregate` and `adminAccounts` are wired to real, existing
|
|
3449
|
+
data sources in this codebase (`@cogenta/agents-builtin`'s security/
|
|
3450
|
+
performance agents, `@cogenta/auth`'s real user/credential model); the
|
|
3451
|
+
rest are honest shape-only placeholders — no real backup mechanism,
|
|
3452
|
+
certificate-expiry check, uptime monitor, or error-aggregation sink
|
|
3453
|
+
exists anywhere yet, and this task does not fabricate one.
|
|
3454
|
+
- `assertNoForbiddenFields` is a real, defense-in-depth runtime scan for the
|
|
3455
|
+
same forbidden list, catching a leak past a loosely-typed call site that
|
|
3456
|
+
TypeScript alone wouldn't stop — the literal "vérification exhaustive de
|
|
3457
|
+
ce qui sort d'un site" security test the lot names.
|
|
3458
|
+
- `signTelemetryPayload`/`verifyTelemetrySignature` reuse `@cogenta/plugins`'
|
|
3459
|
+
generalized Ed25519 primitive (task 9/12) — the same one L8 task 1's
|
|
3460
|
+
pairing already uses — and refuse to sign a payload carrying a forbidden
|
|
3461
|
+
field at all, rather than catching it only closer to the network boundary.
|
|
3462
|
+
- `fingerprintSbom` hashes the real SBOM via the same canonical, sorted-key
|
|
3463
|
+
content-signing helper, with a real bug fixed during this task's own
|
|
3464
|
+
testing: `canonicalizeContent` sorts object keys but not array element
|
|
3465
|
+
order, so two functionally-identical SBOMs built from a `dependencies`
|
|
3466
|
+
record whose keys simply iterate in a different order would otherwise
|
|
3467
|
+
fingerprint differently — the entries are now sorted by name before
|
|
3468
|
+
canonicalizing.
|
|
3469
|
+
|
|
3470
|
+
One new `@cogenta/core` error code: `FLEET_TELEMETRY_FORBIDDEN_FIELD`.
|
|
3471
|
+
|
|
3472
|
+
- [`22ec8de`](https://github.com/cogenta-cms/cogenta/commit/22ec8deec494a2925a943550fcf3c5e1689eb40e) Thanks [@georgesmomo](https://github.com/georgesmomo)! - The MySQL/MariaDB driver's `transaction()` now retries automatically (up to
|
|
3473
|
+
3 attempts) when InnoDB reports `ER_LOCK_DEADLOCK`. A deadlock victim is not
|
|
3474
|
+
an application bug — MySQL expects the losing transaction to restart from
|
|
3475
|
+
scratch — but until now the raw error propagated straight to the caller,
|
|
3476
|
+
so two concurrent writers touching the same rows (two agents publishing at
|
|
3477
|
+
once, not just @cogenta/schema's own ten-concurrent-insert test) could
|
|
3478
|
+
surface a hard failure instead of one of them transparently retrying.
|
|
3479
|
+
|
|
3480
|
+
Found via CI: @cogenta/schema's cursor-pagination-under-concurrent-insert
|
|
3481
|
+
integration test was deterministically deadlocking on both the mysql and
|
|
3482
|
+
mariadb dialects (they share this same driver), not a flake — reproduced
|
|
3483
|
+
on two separate CI runs before the fix.
|
|
3484
|
+
|
|
3485
|
+
- [`e8692eb`](https://github.com/cogenta-cms/cogenta/commit/e8692eba9f47a7a7eee176058f4638abec71dce0) Thanks [@georgesmomo](https://github.com/georgesmomo)! - L7 task 4: the real, capability-gated SDK a sandboxed plugin actually calls.
|
|
3486
|
+
A small starter set — `content.read`, `http.fetch:<domain>`,
|
|
3487
|
+
`storage.read:<prefix>`/`storage.write:<prefix>` — each backed by a real
|
|
3488
|
+
host-side handler (`packages/plugins/src/host/capabilities.ts`) reached
|
|
3489
|
+
through a real bidirectional RPC extension of task 3's message protocol
|
|
3490
|
+
(`sdk-call`/`sdk-result`/`sdk-error`).
|
|
3491
|
+
|
|
3492
|
+
Every handler re-verifies the SPECIFIC request (the exact requested domain,
|
|
3493
|
+
the exact storage key) against the SPECIFIC granted capability parameter —
|
|
3494
|
+
never just "was this capability name granted at all." A plugin granted
|
|
3495
|
+
`http.fetch:api.example.com` cannot use its own SDK method to reach a
|
|
3496
|
+
different domain; a plugin granted `storage.write:plugins/<name>` cannot
|
|
3497
|
+
escape that prefix, including via `../` traversal.
|
|
3498
|
+
|
|
3499
|
+
"Une méthode non accordée est absente de l'objet SDK, pas seulement
|
|
3500
|
+
refusée" (explicit acceptance criterion) is enforced structurally: the
|
|
3501
|
+
guest-side sandbox (`packages/plugins/src/guest/sandbox-entry.mjs`) only
|
|
3502
|
+
ever assigns a method key onto the `sdk` object for a capability actually
|
|
3503
|
+
present in the granted list — a non-granted method is a genuinely missing
|
|
3504
|
+
object key, not a present function that throws.
|
|
3505
|
+
|
|
3506
|
+
One new `@cogenta/core` error code: `PLUGIN_CAPABILITY_REFUSED`.
|
|
3507
|
+
|
|
3508
|
+
- [`6a84427`](https://github.com/cogenta-cms/cogenta/commit/6a84427da789abdce1f61feeef7c1ff5bc7fb9f5) Thanks [@georgesmomo](https://github.com/georgesmomo)! - `create-cogenta` — AI skin generation with hard-refusal validation (L9 task
|
|
3509
|
+
7). When an LLM provider is configured with a valid key and a free-text site
|
|
3510
|
+
description is given, the `blog` blueprint's `theme.tokens.json` is generated
|
|
3511
|
+
from that description instead of copying the theme's default: the model is
|
|
3512
|
+
asked for contract D's token JSON only (never CSS), and every candidate is
|
|
3513
|
+
checked by `@cogenta/render`'s existing `validateSkin` — reused wholesale,
|
|
3514
|
+
not reimplemented — in hard-refusal mode (AA contrast on every pair, a
|
|
3515
|
+
strictly monotone type scale, the full closed token set, `motion.reduced`).
|
|
3516
|
+
On a rejection, the thrown `CogentaError`'s `message`/`hint` become the next
|
|
3517
|
+
attempt's correction prompt, for three attempts. A successful candidate is
|
|
3518
|
+
rendered on three real preview pages (through the same generic
|
|
3519
|
+
`renderPage`/`renderBlock` pipeline a live site uses) written to
|
|
3520
|
+
`.cogenta/skin-preview/`, and the installer offers accept, regenerate, or
|
|
3521
|
+
fall back to the default — bounded so a non-interactive `--yes`/`--config`
|
|
3522
|
+
run never loops. Every outcome — generated and accepted, regenerated,
|
|
3523
|
+
fallen back after failed validation, or never offered — is reported by name
|
|
3524
|
+
in the install recap; nothing is silent.
|
|
3525
|
+
|
|
3526
|
+
Scoped to the `blog` blueprint only, the one blueprint that writes a
|
|
3527
|
+
`theme.tokens.json` today. Regenerating a skin after install (`cogenta skin
|
|
3528
|
+
generate`) is explicit CLI surface the lot doc lists under a later task (L9
|
|
3529
|
+
task 9) and is not built here.
|
|
3530
|
+
|
|
3531
|
+
One new `@cogenta/core` error code: `SKIN_GENERATION_RESPONSE_NOT_JSON`, for
|
|
3532
|
+
a model response that is not a single JSON object.
|