domma-cms 1.6.59 → 1.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 +4344 -0
- package/README.md +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/draft-owner.js +1 -0
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +2 -2
- package/admin/js/lib/page-editor-reference.js +5 -5
- package/admin/js/lib/shortcode-tags.js +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +39 -0
- package/admin/js/views/user-editor.js +1 -1
- package/docs/DommaFeatures.md +3005 -0
- package/docs/MultiTenant.md +176 -0
- package/docs/api-reference.md +1667 -0
- package/docs/cache.md +96 -0
- package/docs/configuration.md +1811 -0
- package/docs/editions-and-licences.md +190 -0
- package/docs/getting-started.md +201 -0
- package/docs/guides/form-triggers.md +260 -0
- package/docs/guides/mongodb-storage-adapter.md +282 -0
- package/docs/guides/multi-page-forms.md +121 -0
- package/docs/images/scribe-composer.png +0 -0
- package/docs/markdown-shortcodes.md +1738 -0
- package/docs/pentest-report-2026-04-03.md +181 -0
- package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
- package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
- package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
- package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
- package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
- package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
- package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
- package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
- package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
- package/docs/plans/2026-03-05-user-types-design.md +139 -0
- package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
- package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
- package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
- package/docs/plans/2026-06-10-core-project-design.md +240 -0
- package/docs/plugin-development.md +827 -0
- package/docs/recruitment-recipe.md +501 -0
- package/docs/scaffolding.md +312 -0
- package/docs/screenshots/calculator-floating.png +0 -0
- package/docs/screenshots/calculator-standard.png +0 -0
- package/docs/screenshots/email-settings.png +0 -0
- package/docs/screenshots/messages-count.png +0 -0
- package/docs/structure/user-types.md +107 -0
- package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
- package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
- package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
- package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
- package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
- package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
- package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
- package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
- package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
- package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
- package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
- package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
- package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
- package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
- package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
- package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
- package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
- package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
- package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
- package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
- package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
- package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
- package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
- package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
- package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
- package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
- package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
- package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
- package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
- package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
- package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
- package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
- package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
- package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
- package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
- package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
- package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
- package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
- package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
- package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
- package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
- package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
- package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
- package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
- package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
- package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
- package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
- package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
- package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
- package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
- package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
- package/docs/theming.md +320 -0
- package/package.json +2 -2
- package/plugins/_lib/admin/mail/keyboard.js +49 -0
- package/plugins/_lib/admin/mail/reader-view.js +4 -2
- package/public/js/celebrations/core/canvas.js +1 -1
- package/public/js/celebrations/index.js +1 -1
- package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
- package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
- package/public/js/celebrations/message-attrs.mjs +1 -0
- package/public/js/celebrations/themes/anniversary.js +1 -0
- package/public/js/celebrations/themes/autumn.js +1 -0
- package/public/js/celebrations/themes/birthday.js +1 -0
- package/public/js/celebrations/themes/butterfly-flight.js +1 -0
- package/public/js/celebrations/themes/calendar.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year.js +1 -0
- package/public/js/celebrations/themes/christmas.js +1 -1
- package/public/js/celebrations/themes/easter.js +1 -0
- package/public/js/celebrations/themes/garden-drawing.js +1 -0
- package/public/js/celebrations/themes/garden.js +1 -0
- package/public/js/celebrations/themes/graduation.js +1 -0
- package/public/js/celebrations/themes/greeting.js +1 -0
- package/public/js/celebrations/themes/make-it-rain.js +1 -0
- package/public/js/celebrations/themes/money-rain.js +1 -0
- package/public/js/celebrations/themes/new-year.js +1 -0
- package/public/js/celebrations/themes/occasion-drawing.js +1 -0
- package/public/js/celebrations/themes/occasions.js +1 -0
- package/public/js/celebrations/themes/rain-control.js +1 -0
- package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
- package/public/js/celebrations/themes/sparkler-font.js +1 -0
- package/public/js/celebrations/themes/sparkler-writing.js +1 -0
- package/public/js/celebrations/themes/summer-garden.js +1 -0
- package/public/js/celebrations/themes/winter.js +1 -0
- package/public/js/celebrations/trait-data.mjs +1 -1
- package/public/js/effects.js +1 -1
- package/scripts/build.js +4 -0
- package/server/services/markdown.js +5 -2
- package/server/templates/page.html +1 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# Content API - headless read access to pages, menus and site content - design
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-10-09
|
|
4
|
+
**Status:** Implemented on `feat/content-api` (unreleased) - see `docs/superpowers/plans/2026-10-09-content-api.md`
|
|
5
|
+
**Builds on:** the external API surface (`/api/v1/:slug`, API tokens, API Builder) - see the
|
|
6
|
+
`domma-api-surface` skill
|
|
7
|
+
|
|
8
|
+
Adds a versioned, externally consumable **Content API** beside the existing **Data API**
|
|
9
|
+
(`/api/v1/:slug`). The Data API exposes collection entries; the Content API exposes what the CMS
|
|
10
|
+
*publishes*: pages, menus, a safe subset of site settings, and search. Its first consumers are
|
|
11
|
+
headless front ends (a Next/Astro/mobile client), a static-site exporter and the Manager.
|
|
12
|
+
|
|
13
|
+
## Goal
|
|
14
|
+
|
|
15
|
+
1. A third-party client can list and fetch published pages, with frontmatter, as Markdown, rendered
|
|
16
|
+
HTML or both.
|
|
17
|
+
2. **One access decision.** A page reachable over the API is reachable on the public site by the
|
|
18
|
+
same viewer, and the reverse - no second implementation of drafts, visibility or the project
|
|
19
|
+
kill-switch.
|
|
20
|
+
3. The same credentials model as the Data API: public, a role (JWT), or a project-bound `dcms_`
|
|
21
|
+
token - with the same strictness rules.
|
|
22
|
+
4. Cached and invalidated by the existing tag cache, with no new invalidation paths.
|
|
23
|
+
|
|
24
|
+
Non-goals for v1: writes (create/update/delete pages), media upload, block/component source,
|
|
25
|
+
theme CSS. See [Phase 2](#phase-2---writes).
|
|
26
|
+
|
|
27
|
+
## What already exists
|
|
28
|
+
|
|
29
|
+
Verified against 0.116.8:
|
|
30
|
+
|
|
31
|
+
- `getPage(urlPath, {user})` / `listPages()` (`server/services/content.js`) return frontmatter,
|
|
32
|
+
`urlPath`, raw `content`, rendered `html`, `usedComponents`, `cacheTags`, `perViewer` - through
|
|
33
|
+
the same `parseMarkdown` pipeline the public site uses. `listPages()` also stamps
|
|
34
|
+
`resolvedProject`.
|
|
35
|
+
- The full public access decision lives inline in the `/*` route at `server/routes/public.js:177`:
|
|
36
|
+
project kill-switch → 404, `status !== 'published'` → 404 unless `canPreviewDrafts`,
|
|
37
|
+
`checkVisibility()` → 403, cache key `page:<url>:m<mtime>:role<roles>:o<baseUrl>` with tags
|
|
38
|
+
`page:<url>`, `nav`, `site` plus the page's own.
|
|
39
|
+
- `resolveViewer(request)` (`server/services/viewerSession.js`) - cookie/JWT viewer resolution.
|
|
40
|
+
- `resolveLocation(slot, user, ctx)` (`server/services/menus.js`) - bound menu for a slot, items
|
|
41
|
+
already filtered for the viewer.
|
|
42
|
+
- `searchPages(pages, query, {viewer, exclusions})` + `isSearchable()` (`server/services/search.js`).
|
|
43
|
+
- `validateToken()`, `scopeAllows()` and `validateScopes()` (`server/services/apiTokens.js`) -
|
|
44
|
+
scopes are `{collection, verbs[]}` only.
|
|
45
|
+
- `checkPublicAccess()` (`server/routes/api/collections.js`) - the token/role/public gate, keyed off
|
|
46
|
+
`schema.api.<verb>`. Pages have no schema, so it cannot be reused as-is.
|
|
47
|
+
- The `onSend` hook in `server/server.js` that marks `/api/*` `no-store` already exempts
|
|
48
|
+
`/api/v1/` and `/api/x/`; the Content API needs the same exemption.
|
|
49
|
+
|
|
50
|
+
## Findings that shape the design
|
|
51
|
+
|
|
52
|
+
1. **`/api/v1/pages` is not free.** `/api/v1/:slug` matches any collection slug, and `slugify()`
|
|
53
|
+
happily produces `pages`, `menus` or `content`. Fastify's static-beats-param rule would silently
|
|
54
|
+
shadow an existing collection of that name. The Content API therefore gets its own prefix.
|
|
55
|
+
2. **The access decision is not extractable by import** - it is inline in a route handler that
|
|
56
|
+
writes HTML replies. It must be lifted into a service first, or the API will drift from the site.
|
|
57
|
+
3. **Two draft rules exist today.** `public.js` hides `status !== 'published'`; `isSearchable()`
|
|
58
|
+
hides only `status === 'draft'`. Any other status (e.g. `scheduled`, `archived`) is hidden on the
|
|
59
|
+
site but searchable. The Content API uses the public rule, and search should be brought into
|
|
60
|
+
line in the same change.
|
|
61
|
+
4. **Rendered HTML is Domma-dependent.** Tabs, carousels, accordions, `[collection]` displays and
|
|
62
|
+
effects emit markup that needs Domma's CSS/JS (and `__CMS_DCONFIG__`) to behave. Clients need to
|
|
63
|
+
be told, not left to discover it.
|
|
64
|
+
5. **Frontmatter is open-ended.** Plugins and authors add keys freely; `dconfig`, `sidebar` and
|
|
65
|
+
`seo` carry implementation detail. Output must be an allowlist, never a spread.
|
|
66
|
+
6. **`site.json` holds SMTP credentials.** No endpoint may serialise it wholesale - including the
|
|
67
|
+
`__CMS_SITE__` object, whose shape should be checked before being reused as the settings payload.
|
|
68
|
+
7. **Per-viewer renders exist.** `[menu]` filters by role and a row-scoped `[view]` renders each
|
|
69
|
+
user their own rows (`perViewer`). A token has no user.
|
|
70
|
+
|
|
71
|
+
## URL surface
|
|
72
|
+
|
|
73
|
+
Prefix: **`/api/content/v1`** (unused today; no collection slug can reach it).
|
|
74
|
+
|
|
75
|
+
| Method | Path | Returns |
|
|
76
|
+
|--------|------------------------------------|---------|
|
|
77
|
+
| `GET` | `/api/content/v1/pages` | Paged list of page summaries |
|
|
78
|
+
| `GET` | `/api/content/v1/pages/*` | One page by URL path (`/pages/` = home, `/pages/services/web-dev`) |
|
|
79
|
+
| `GET` | `/api/content/v1/menus/:location` | The menu bound to a location, filtered for the caller |
|
|
80
|
+
| `GET` | `/api/content/v1/site` | Public site settings subset |
|
|
81
|
+
| `GET` | `/api/content/v1/search?q=` | Search results |
|
|
82
|
+
|
|
83
|
+
### Pages list
|
|
84
|
+
|
|
85
|
+
Query: `page`, `limit` (default 50, max 200 - and `limit=0` means the default, matching
|
|
86
|
+
`/api/v1`), `prefix` (URL path prefix, e.g. `/services`), `tag`, `category`, `project`, `sort`
|
|
87
|
+
(`sortOrder` default | `title` | `updatedAt` | `createdAt`), `order`.
|
|
88
|
+
|
|
89
|
+
Response mirrors the Data API list shape:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"pages": [
|
|
94
|
+
{ "urlPath": "/about", "title": "About", "description": "…", "tags": ["company"],
|
|
95
|
+
"category": "general", "layout": "default", "updatedAt": "2026-09-30T10:12:00.000Z" }
|
|
96
|
+
],
|
|
97
|
+
"total": 14, "page": 1, "limit": 50
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Summaries never include a body - the list must stay cheap enough to run over every page without
|
|
102
|
+
rendering any.
|
|
103
|
+
|
|
104
|
+
### Single page
|
|
105
|
+
|
|
106
|
+
`GET /api/content/v1/pages/about?format=html|markdown|both` (default `html`).
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"urlPath": "/about",
|
|
111
|
+
"title": "About",
|
|
112
|
+
"description": "…",
|
|
113
|
+
"layout": "default",
|
|
114
|
+
"tags": ["company"],
|
|
115
|
+
"category": "general",
|
|
116
|
+
"seo": { "title": "…", "description": "…", "image": "…" },
|
|
117
|
+
"createdAt": "…", "updatedAt": "…",
|
|
118
|
+
"html": "<section>…</section>",
|
|
119
|
+
"markdown": "## About us\n…",
|
|
120
|
+
"requires": { "domma": true, "components": ["tabs", "carousel"] },
|
|
121
|
+
"perViewer": false
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- `html` is the body only (`page.html`), not the full rendered document with navbar and footer.
|
|
126
|
+
- `requires.components` names the Domma markup in the body that `public/js/site.js` initialises
|
|
127
|
+
(tabs, accordion, carousel, countdown, tooltip, reveal, …), plus any custom `[component]`s.
|
|
128
|
+
`usedComponents` alone is not enough: it lists only custom components, and a page of tabs
|
|
129
|
+
reports none. `requires.domma` is true when the list is non-empty, so a client can decide
|
|
130
|
+
whether to load Domma or render the Markdown itself.
|
|
131
|
+
- `markdown` is the raw source. It contains shortcodes, which are documented in
|
|
132
|
+
`docs/markdown-shortcodes.md`; a client choosing Markdown takes on interpreting them.
|
|
133
|
+
- `seo` is allowlisted (`title`, `description`, `image`, `canonical`, `noindex`) - not passed through.
|
|
134
|
+
|
|
135
|
+
Resolution follows the public route: `/services` falls back to `/services/index`.
|
|
136
|
+
|
|
137
|
+
### Menus
|
|
138
|
+
|
|
139
|
+
`GET /api/content/v1/menus/navbar?page=/about` - `resolveLocation(slot, viewer, {urlPath})`.
|
|
140
|
+
Returns `{location, slug, name, items}` with items in the same nested `items` shape the navbar
|
|
141
|
+
reads, each item allowlisted (`type`, `text`, `url`, `icon`, `target`, `size`, `badge`, `items`) -
|
|
142
|
+
none of its gating. Structural items (`separator`, `spacer`) are kept: a client rendering them is
|
|
143
|
+
better than indices shifting. The `admin-sidebar` and `overlay` slots, and any slot starting
|
|
144
|
+
`admin`, are never served (404).
|
|
145
|
+
|
|
146
|
+
### Site
|
|
147
|
+
|
|
148
|
+
A fixed allowlist built in one function (`publicSiteSettings()`), never a filtered copy of
|
|
149
|
+
`site.json`, holding exactly what every public page already shows: `title`, `brand` (text, logo,
|
|
150
|
+
url), `footer` (hidden links removed, as the renderer does), `social`, the SEO defaults in the meta
|
|
151
|
+
tags (`defaultTitle`, `titleSeparator`, `defaultDescription`) and the `theme` id. `tagline` and
|
|
152
|
+
`url` never reach a public page, so they are left out. Anything added later is added to the
|
|
153
|
+
allowlist explicitly.
|
|
154
|
+
|
|
155
|
+
### Search
|
|
156
|
+
|
|
157
|
+
`GET /api/content/v1/search?q=…&limit=10` - `searchPages(listPages(), q, {viewer, exclusions})`,
|
|
158
|
+
returning `{results: [{urlPath, title, excerpt, score}]}`. Respects `config/search.json`
|
|
159
|
+
`enabled: false` (404).
|
|
160
|
+
|
|
161
|
+
## Access
|
|
162
|
+
|
|
163
|
+
### Configuration
|
|
164
|
+
|
|
165
|
+
New file **`config/content-api.json`** (yours; absence = disabled):
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"enabled": true,
|
|
170
|
+
"access": "public",
|
|
171
|
+
"resources": {
|
|
172
|
+
"pages": { "enabled": true },
|
|
173
|
+
"menus": { "enabled": true },
|
|
174
|
+
"site": { "enabled": true },
|
|
175
|
+
"search": { "enabled": true }
|
|
176
|
+
},
|
|
177
|
+
"formats": ["html", "markdown"],
|
|
178
|
+
"listable": true
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- `access` is `"public"`, `"token"` or a role name - the same three modes and the same ladder /
|
|
183
|
+
unknown-role-fails-closed rule as `schema.api.<verb>.access`.
|
|
184
|
+
- `formats` lets a site refuse Markdown (shortcode source can reveal collection slugs, view names
|
|
185
|
+
and form ids that the HTML does not).
|
|
186
|
+
- `listable: false` keeps single-page fetch but turns the list off, for sites that do not want
|
|
187
|
+
their full URL inventory enumerable.
|
|
188
|
+
- **Off by default.** An upgrade must not expose anything a site has not chosen to.
|
|
189
|
+
|
|
190
|
+
Edited in the admin at **System → API Tokens**, on a new "Content API" tab, or a small screen of
|
|
191
|
+
its own - see [Open questions](#open-questions).
|
|
192
|
+
|
|
193
|
+
### The gate
|
|
194
|
+
|
|
195
|
+
`checkContentAccess(resource, request, reply)` - a sibling of `checkPublicAccess`, reading
|
|
196
|
+
`config/content-api.json` instead of a schema. The token branch is shared, not copied: factor the
|
|
197
|
+
Bearer-parse → `validateToken` → project check → scope check out of `checkPublicAccess` into
|
|
198
|
+
`authenticateApiToken(request, {project, scope})`, used by both gates.
|
|
199
|
+
|
|
200
|
+
Strictness carries over unchanged: a token never satisfies a role mode, a JWT never satisfies
|
|
201
|
+
token mode.
|
|
202
|
+
|
|
203
|
+
### Tokens and projects
|
|
204
|
+
|
|
205
|
+
- A token is bound to one project. For pages, the project is `getProjectForPage()`; a token for
|
|
206
|
+
`world-cup` lists and fetches only pages resolving to `world-cup` (`core` tokens see `core`
|
|
207
|
+
pages) - single pages, the list and search results alike; a page in another project is `404`.
|
|
208
|
+
Menus and site settings are site-wide and already shown on every public page, so any valid
|
|
209
|
+
token may read them. The gate binds no project itself (`authenticateApiToken` without
|
|
210
|
+
`project`); the routes bind per page.
|
|
211
|
+
- **Scopes gain a second shape:** `{ "content": "pages" | "menus" | "site" | "search" }`. A scope
|
|
212
|
+
is `{collection, verbs}` OR `{content}`. `validateScopes()` accepts both; `scopeAllows()` gains a
|
|
213
|
+
`contentScopeAllows(scopes, resource)` sibling. Empty scopes still mean "everything in the
|
|
214
|
+
project" - which now includes content. That widens existing unscoped tokens, so it is gated on
|
|
215
|
+
the site enabling `access: "token"` - an unscoped token gets content only on sites that opted in.
|
|
216
|
+
|
|
217
|
+
### Viewer and per-viewer content
|
|
218
|
+
|
|
219
|
+
- Public and token callers render as **anonymous** (`user: null`). Role-mode callers render as
|
|
220
|
+
themselves.
|
|
221
|
+
- Role mode accepts a Bearer **access** JWT only (a refresh token is refused) - never the
|
|
222
|
+
`dm_session` cookie, which only the page renderer reads, so no API answers to ambient
|
|
223
|
+
credentials.
|
|
224
|
+
- A page with `perViewer: true` is served as rendered for that viewer, and the flag is returned so
|
|
225
|
+
a client knows the body is not shareable between users.
|
|
226
|
+
- Drafts: never served in public or token mode. In role mode a caller holding `pages.read` may
|
|
227
|
+
pass `?preview=1` to receive drafts, mirroring `canPreviewDrafts` - responses are then
|
|
228
|
+
`no-store` and never cached.
|
|
229
|
+
|
|
230
|
+
## The shared page resolver
|
|
231
|
+
|
|
232
|
+
Lift the decision out of `public.js` into `server/services/pagePublishing.js`:
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
/**
|
|
236
|
+
* @returns {Promise<
|
|
237
|
+
* {status: 'ok', page, project, isDraft} |
|
|
238
|
+
* {status: 'not-found'} | {status: 'forbidden'}
|
|
239
|
+
* >}
|
|
240
|
+
*/
|
|
241
|
+
export async function resolvePublishedPage(urlPath, viewer, {allowDrafts = false} = {})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
It does the index fallback, the project kill-switch, the draft rule and `checkVisibility()`, and
|
|
245
|
+
returns a verdict - no replies, no HTML. `public.js` keeps its rendering and cache key and calls
|
|
246
|
+
this for the decision; the Content API calls it too. **This refactor lands first, on its own, with
|
|
247
|
+
the existing public-route tests passing unchanged** - it is the step that makes "one access
|
|
248
|
+
decision" true rather than aspirational.
|
|
249
|
+
|
|
250
|
+
`listPages()` filtering for the list endpoint uses the same predicate (`isPubliclyListed(page,
|
|
251
|
+
viewer)`), and `isSearchable()` is changed to call it - closing finding #3.
|
|
252
|
+
|
|
253
|
+
## Caching
|
|
254
|
+
|
|
255
|
+
- **No server-side response cache.** The access decision has to parse the page on every request -
|
|
256
|
+
exactly as the site's does - and the JSON is built straight from that parse, so caching the
|
|
257
|
+
payload would save nothing. (The site caches its *rendered document*, which is the expensive
|
|
258
|
+
part; the API has no document.)
|
|
259
|
+
- Page writes clear a `pages:list` tag (not `pages`, which `cache-invalidation.test.js` pins as
|
|
260
|
+
carrying nothing), ready for when a list cache is worth having.
|
|
261
|
+
- Token responses: `Cache-Control: private, no-cache` so intermediaries never share a
|
|
262
|
+
token-gated body.
|
|
263
|
+
- Public responses: `Cache-Control: public, max-age=60` plus a weak `ETag` from mtime.
|
|
264
|
+
- Role and preview responses: `no-store`.
|
|
265
|
+
- Refusals (404/401/403/400): `no-store`, set by a hook scoped to the Content API routes - a 404
|
|
266
|
+
with no `Cache-Control` is heuristically cacheable and would outlive switching the API on.
|
|
267
|
+
- `server.js` `onSend`: add `/api/content/v1/` to the exempt prefixes.
|
|
268
|
+
|
|
269
|
+
## Errors
|
|
270
|
+
|
|
271
|
+
Same as the Data API, with no oracle:
|
|
272
|
+
|
|
273
|
+
| Case | Status |
|
|
274
|
+
|------|--------|
|
|
275
|
+
| Content API disabled, resource disabled, page missing, draft, disabled project | `404` |
|
|
276
|
+
| Token/JWT missing or invalid | `401` |
|
|
277
|
+
| Token wrong project or scope, visibility refused | `403` - except a page outside the token's project, which is `404` (the project is the token's whole world) |
|
|
278
|
+
| Format not allowed | `400` |
|
|
279
|
+
|
|
280
|
+
A visibility-gated page answers `403` exactly as the site does, so the API leaks no more than the
|
|
281
|
+
site already does.
|
|
282
|
+
|
|
283
|
+
## Testing
|
|
284
|
+
|
|
285
|
+
- `pagePublishing.js` unit tests: every verdict, including index fallback, unknown role in
|
|
286
|
+
visibility, `=role` exact match, disabled project, non-`published` statuses.
|
|
287
|
+
- Public route regression: the existing suite passes unchanged after the extraction.
|
|
288
|
+
- Parity test: for a fixture site, every page × {anon, user, admin} gets the same allow/deny from
|
|
289
|
+
`GET /about` and `GET /api/content/v1/pages/about`.
|
|
290
|
+
- Token tests: project binding, `{content}` scopes, unscoped token with Content API off/on,
|
|
291
|
+
JWT refused in token mode.
|
|
292
|
+
- Leakage tests: unknown frontmatter keys never appear; `site` never contains `smtp`; Markdown
|
|
293
|
+
refused when `formats` excludes it.
|
|
294
|
+
- Cache: a page save changes the next API response; a token response carries `private`.
|
|
295
|
+
|
|
296
|
+
## Build order
|
|
297
|
+
|
|
298
|
+
1. Extract `resolvePublishedPage` / `isPubliclyListed`; switch `public.js` and `isSearchable()`.
|
|
299
|
+
2. Factor `authenticateApiToken()` out of `checkPublicAccess()`.
|
|
300
|
+
3. `config/content-api.json` loader + `checkContentAccess()`.
|
|
301
|
+
4. Routes (`server/routes/api/content-public.js`), pages first, then menus, site, search.
|
|
302
|
+
5. Token scope shape `{content}` - service, admin token editor, scaffolder `apiTokens` block.
|
|
303
|
+
6. Admin settings UI.
|
|
304
|
+
7. Docs: `docs/api-reference.md` section, in-admin API reference page, `domma-api-surface` skill,
|
|
305
|
+
`FEATURES.md`, `CHANGELOG.md`.
|
|
306
|
+
|
|
307
|
+
## Phase 2 - writes
|
|
308
|
+
|
|
309
|
+
Not in v1, recorded so v1 does not paint it out:
|
|
310
|
+
|
|
311
|
+
- `POST/PUT/DELETE /api/content/v1/pages/*` through `createPage` / `updatePage` / `deletePage`,
|
|
312
|
+
so versioning (`content/versions`), audit and cache invalidation come for free.
|
|
313
|
+
- Token-only (no public writes, ever), with a `{content: "pages", verbs: [...]}` scope - which is
|
|
314
|
+
why the scope shape above is an object rather than a bare string.
|
|
315
|
+
- Author attribution `token:<id>`, as the Data API does for entries.
|
|
316
|
+
- Media upload, blocks and menus as separate, later decisions.
|
|
317
|
+
|
|
318
|
+
## Settled questions
|
|
319
|
+
|
|
320
|
+
Settled in the plan and kept through implementation:
|
|
321
|
+
|
|
322
|
+
1. **Admin home:** its own screen, System › Content API (`#/content-api`, permission `settings`).
|
|
323
|
+
2. **Per-page opt-out:** yes - front matter `api: false` (404, not listed, not searched).
|
|
324
|
+
3. **Asset URLs in `requires`:** not in v1. `requires` names components only.
|
|
325
|
+
4. **Edition:** the same as the Data API - free core (`docs/editions-and-licences.md`).
|