domma-cms 1.6.60 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +4349 -0
  2. package/README.md +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/js/lib/effect-defs.js +1 -1
  6. package/admin/js/lib/effects-builder.js +2 -2
  7. package/admin/js/lib/mail-compose.js +1 -0
  8. package/admin/js/lib/page-editor-reference.js +5 -5
  9. package/admin/js/lib/shortcode-tags.js +1 -1
  10. package/admin/js/templates/call-new.html +1 -1
  11. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  12. package/admin/js/views/call-new.js +1 -1
  13. package/admin/js/views/contacts.js +15 -15
  14. package/admin/js/views/form-submissions.js +2 -2
  15. package/docs/DommaFeatures.md +3005 -0
  16. package/docs/MultiTenant.md +176 -0
  17. package/docs/api-reference.md +1667 -0
  18. package/docs/cache.md +96 -0
  19. package/docs/configuration.md +1811 -0
  20. package/docs/editions-and-licences.md +190 -0
  21. package/docs/getting-started.md +201 -0
  22. package/docs/guides/form-triggers.md +260 -0
  23. package/docs/guides/mongodb-storage-adapter.md +282 -0
  24. package/docs/guides/multi-page-forms.md +121 -0
  25. package/docs/images/scribe-composer.png +0 -0
  26. package/docs/markdown-shortcodes.md +1738 -0
  27. package/docs/pentest-report-2026-04-03.md +181 -0
  28. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  29. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  30. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  31. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  32. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  33. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  34. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  35. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  36. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  37. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  38. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  39. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  40. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  41. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  42. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  43. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  44. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  45. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  46. package/docs/plugin-development.md +827 -0
  47. package/docs/recruitment-recipe.md +501 -0
  48. package/docs/scaffolding.md +312 -0
  49. package/docs/screenshots/calculator-floating.png +0 -0
  50. package/docs/screenshots/calculator-standard.png +0 -0
  51. package/docs/screenshots/email-settings.png +0 -0
  52. package/docs/screenshots/messages-count.png +0 -0
  53. package/docs/structure/user-types.md +107 -0
  54. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  55. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  56. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  57. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  58. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  59. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  60. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  61. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  62. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  63. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  64. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  65. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  66. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  67. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  68. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  69. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  70. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  71. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  72. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  73. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  74. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  75. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  76. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  77. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  78. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  79. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  80. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  81. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  82. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  83. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  84. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  85. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  86. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  87. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  88. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  89. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  90. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  91. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  92. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  93. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  94. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  95. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  96. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  97. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  98. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  99. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  100. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  101. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  102. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  103. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  104. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  105. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  106. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  107. package/docs/theming.md +320 -0
  108. package/package.json +2 -2
  109. package/plugins/_lib/admin/mail/compose-window.js +3 -1
  110. package/plugins/_lib/admin/mail/identity.js +18 -3
  111. package/plugins/_lib/admin/mail/reader-view.js +32 -3
  112. package/plugins/blog/CLAUDE.md +4 -0
  113. package/plugins/blog/admin/lib/email.js +191 -0
  114. package/plugins/blog/admin/views/comments.js +3 -1
  115. package/plugins/blog/plugin.json +2 -2
  116. package/plugins/blog/plugin.public.js +1 -1
  117. package/plugins/blog/tests/email.test.js +69 -0
  118. package/plugins/booking/CLAUDE.md +4 -0
  119. package/plugins/booking/admin/lib/email.js +191 -0
  120. package/plugins/booking/admin/views/panels.js +7 -3
  121. package/plugins/booking/plugin.json +1 -1
  122. package/plugins/booking/tests/email.test.js +69 -0
  123. package/plugins/free-tier.lock.json +27 -19
  124. package/plugins/shopping-cart/CLAUDE.md +4 -0
  125. package/plugins/shopping-cart/admin/lib/email.js +191 -0
  126. package/plugins/shopping-cart/admin/views/orders.js +11 -3
  127. package/plugins/shopping-cart/plugin.json +2 -2
  128. package/plugins/shopping-cart/tests/email.test.js +69 -0
  129. package/plugins/support/CLAUDE.md +4 -0
  130. package/plugins/support/admin/lib/email.js +191 -0
  131. package/plugins/support/admin/views/desk.js +4 -1
  132. package/plugins/support/admin/views/inbox.js +6 -2
  133. package/plugins/support/plugin.json +2 -2
  134. package/plugins/support/tests/email.test.js +69 -0
  135. package/public/js/celebrations/core/canvas.js +1 -1
  136. package/public/js/celebrations/index.js +1 -1
  137. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  138. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  139. package/public/js/celebrations/message-attrs.mjs +1 -0
  140. package/public/js/celebrations/themes/anniversary.js +1 -0
  141. package/public/js/celebrations/themes/autumn.js +1 -0
  142. package/public/js/celebrations/themes/birthday.js +1 -0
  143. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  144. package/public/js/celebrations/themes/calendar.js +1 -0
  145. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  146. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  147. package/public/js/celebrations/themes/christmas.js +1 -1
  148. package/public/js/celebrations/themes/easter.js +1 -0
  149. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  150. package/public/js/celebrations/themes/garden.js +1 -0
  151. package/public/js/celebrations/themes/graduation.js +1 -0
  152. package/public/js/celebrations/themes/greeting.js +1 -0
  153. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  154. package/public/js/celebrations/themes/money-rain.js +1 -0
  155. package/public/js/celebrations/themes/new-year.js +1 -0
  156. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  157. package/public/js/celebrations/themes/occasions.js +1 -0
  158. package/public/js/celebrations/themes/rain-control.js +1 -0
  159. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  160. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  161. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  162. package/public/js/celebrations/themes/summer-garden.js +1 -0
  163. package/public/js/celebrations/themes/winter.js +1 -0
  164. package/public/js/celebrations/trait-data.mjs +1 -1
  165. package/public/js/effects.js +1 -1
  166. package/scripts/build.js +4 -0
  167. package/server/services/markdown.js +5 -2
  168. package/server/templates/page.html +1 -1
@@ -0,0 +1,505 @@
1
+ # Marketplace Step 2 - Consolidation, Extraction and Withdrawal
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Put every plugin in one repo, extract `invoice` and `surveys` from the free tier without
6
+ disturbing a live site, stop the scaffolder seeding paid plugins, withdraw `seo-pro`, and get the
7
+ five unversioned site plugins into git.
8
+
9
+ **Architecture:** A new private monorepo `dcms-marketplace` becomes the canonical home for every
10
+ marketplace plugin, free and paid. `domma-cms/plugins/` keeps a vendored copy of the free tier only
11
+ (so `npm install domma-cms` stays self-contained), synced from the monorepo. The manager's
12
+ `paid-plugins/` becomes a checkout rather than a source. Every manifest gains `tier`, and the
13
+ scaffolder's hand-written `SKIP_PLUGINS` set is replaced by a read of that field.
14
+
15
+ **Spec:** `docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md` (sequencing step 2)
16
+
17
+ **Tech Stack:** Fastify 5 (ESM), Domma SPA admin, `node:test` runner, JSON file storage, git.
18
+
19
+ **Critical project rules** (from `CLAUDE.md` + memory):
20
+
21
+ - Server on port **4096** is **always running** - never start or stop it. Test via `curl`.
22
+ - **Never stop a fleet site by hand.** Use `processManager` through the Site Manager API or
23
+ `entitlements.rescindPlugin()`. A site stopped outside `processManager` after a pm2 manager restart
24
+ becomes an orphan and does not respawn.
25
+ - Fleet sites run `NODE_ENV=production`, so cached HTML ignores hand-edited data until the site
26
+ restarts. Plugin file changes need a restart too.
27
+ - `domma-cms` is worked on concurrently by another agent. **Commit early, add named paths only,
28
+ never stash.**
29
+ - `npm test` is serial on purpose (`--test-concurrency=1`). Restore any config file a test pollutes.
30
+ - Do **not** publish to npm as part of this plan. Roll locally: build to `_publish/` and use
31
+ `SOURCE=` that directory.
32
+
33
+ **Ordering constraints that must not be reordered:**
34
+
35
+ 1. **Grandfather before extract.** Licences for `invoice` and `surveys` are issued *before* either is
36
+ removed from `domma-cms/plugins/`, so no site ever holds a paid plugin with no licence record.
37
+ 2. **`proBundle` before revoke.** `seo-pro` leaves the Pro bundle *before* its licences are revoked,
38
+ or the next Pro enable re-issues one.
39
+ 3. **Verify provenance before importing.** Several plugins exist in three places and only one copy is
40
+ the source; importing a minified derivative is silent and permanent.
41
+
42
+ ---
43
+
44
+ ## Phase 0 - Provenance verification (no writes)
45
+
46
+ The monorepo must be seeded from source, never from a build artifact. Two derivations are already
47
+ known: the manager's copies of plugins that also live in `domma-cms` are built (their
48
+ `admin/css/index.css` is a single line against 87 to 487 lines in `domma-cms`), and every site copy
49
+ has been through `pluginDistributor.minifyAdminCode()` at `activate()`.
50
+
51
+ - [x] For each slug, record the canonical copy in a provenance table committed to the monorepo as
52
+ `PROVENANCE.md`. **Done 2026-09-22** - findings below; the file is drafted and lands with Phase 1:
53
+ - **Free set + `invoice` + `surveys`** → `domma-cms/plugins/<slug>/` (unminified, carries CLAUDE.md)
54
+ - **Paid set** (`analytics-pro`, `data-transfer`, `mongodb-adapter`, `job-board`, `theme-roller`) →
55
+ `domma-cms-manager/plugins/site-manager/paid-plugins/<slug>/`
56
+ - **Manager-only** (`calendar`, `docs`, `messages`, `garage`) → `domma-cms-manager/plugins/<slug>/`
57
+ - [x] **`email-pro` resolved.** Its two copies (`domma-cms-manager/plugins/email-pro` and
58
+ `paid-plugins/email-pro`) are byte-identical, so there is nothing to choose. Take `paid-plugins/`
59
+ as canonical, since that is the distribution source. Paid, sold under licence.
60
+ - [x] Confirm no site copy is newer than its claimed source. **Done** - every file in every
61
+ candidate copy hashed, excluding `data/`, `backups/` and `node_modules/`.
62
+
63
+ **Exit: met.** `PROVENANCE.md` names one source per slug and every difference is accounted for.
64
+
65
+ ### Phase 0 findings
66
+
67
+ **1. `paid-plugins/surveys` is a stale fork and must not be imported.** This is what the phase was
68
+ for. Three copies existed and the one that looked authoritative was three months behind:
69
+
70
+ | Copy | `plugin.js` | mtime |
71
+ |---|---|---|
72
+ | `domma-cms/plugins/surveys` | 9,368 bytes | 2026-08-21 |
73
+ | `domma-cms-manager/plugins/surveys` | 9,368 bytes | 2026-09-22 (build copy of the above) |
74
+ | `paid-plugins/surveys` | **8,985 bytes** | **2026-06-29** |
75
+
76
+ `domma-cms` against `paid-plugins`: **16 files differ**, including `plugin.js`, `lib/ledger.js`,
77
+ `lib/sending.js`, `lib/submit.js` and two admin templates. Importing the wrong one would have rolled
78
+ `surveys` back silently, with no error anywhere. Canonical source is **`domma-cms/plugins/surveys`**.
79
+ No site runs the stale copy: every site receives `surveys` from the scaffolder, which copies the
80
+ manager's build of the `domma-cms` source, and it is enabled on zero sites.
81
+
82
+ **2. Nothing has ever been hand-edited on a live site.** All 18 scaffolder-distributed plugins are
83
+ byte-identical across all 8 sites and match their manager source exactly. `email-pro` is the only
84
+ exception and only in 8 minified `admin/**/*.js` files, which is `minifyAdminCode()` behaving as
85
+ designed. **There are no live fixes to rescue**, which removes the risk that made Phase 5's deletions
86
+ dangerous.
87
+
88
+ **3. `minifyAdminCode()` has only ever run for `email-pro`.** `analytics-pro`, `data-transfer`,
89
+ `mongodb-adapter` and `seo-pro` are all `closedSource: true`, yet their `admin/**/*.js` is byte
90
+ identical to source on every site they are installed on - so their admin code ships readable. Either
91
+ they were granted before that step existed, or esbuild was unavailable at grant time. Not a security
92
+ matter, since it never was one, but re-granting them would apply it. Worth deciding whether to
93
+ re-grant during Phase 5, when those sites are being touched anyway.
94
+
95
+ **4. `email-pro`'s two copies are byte-identical**, so there was nothing to choose. `paid-plugins/` is
96
+ canonical as the distribution source.
97
+
98
+ **5. The monorepo needs `.gitignore` entries for `data/` and `backups/`.** `data-transfer` writes
99
+ timestamped backup trees into `backups/` at runtime; `oneill-leisure` holds 28 such files.
100
+
101
+ ---
102
+
103
+ ## Phase 1 - Create the monorepo
104
+
105
+ **Done 2026-09-22** at `/home/dommajs/domains/dcms-marketplace`, 22 commits, no remote yet.
106
+
107
+ - [x] Repo created with the spec's layout, plus `README.md` and `scripts/verify.js`.
108
+ - [x] All **20** plugins imported from their Phase 0 sources, one verbatim commit each naming the
109
+ absolute source path. Re-verified afterwards: every one is byte-identical to its source, manifest
110
+ excepted.
111
+ - [x] `_lib` and `_template` not imported - they stay in `domma-cms/plugins/` so the builder works
112
+ from `npm install` alone.
113
+ - [x] `site-manager`, `customer-registration`, `site-tweaks` and the four site-local plugins not
114
+ imported.
115
+ - [x] `tier` on every manifest: **8 free, 12 paid**.
116
+ - [x] `closedSource: true` added - **to six plugins, not two**. See the deviation below.
117
+
118
+ **Exit: met.** `make verify` passes: 20 plugins, 8 free, 12 paid, 4 reserved slugs, required manifest
119
+ fields present, no committed runtime state.
120
+
121
+ ### Deviation: closedSource on six plugins, not two
122
+
123
+ This phase called for `closedSource: true` on `invoice` and `surveys` only. It was also applied to
124
+ `calendar`, `docs`, `messages` and `garage`, which are open in their manager source but are becoming
125
+ paid. `closedSource` is what refuses the source editor (`pluginFiles.isClosedSource`) and what makes
126
+ path C refuse to export a plugin, so a paid plugin without it is readable and re-exportable by the
127
+ customer who bought it - which would contradict the design's own export refusal. Deployed copies are
128
+ unaffected until they are re-distributed.
129
+
130
+ `scripts/verify.js` now enforces this as an invariant: a `tier: "paid"` plugin without
131
+ `closedSource: true` fails the check.
132
+
133
+ ### What the import commits are for
134
+
135
+ Each import is a pristine snapshot; `tier` and `closedSource` were applied afterwards in one separate
136
+ commit. So `git show <import-commit>` remains exactly what the source looked like on 2026-09-22,
137
+ which is what makes the Phase 0 provenance claims checkable later rather than merely asserted.
138
+
139
+ ### Remote
140
+
141
+ Not configured - the user is setting it up. After `git remote add origin …`, push `main`. Nothing in
142
+ the repo assumes a remote.
143
+
144
+ ---
145
+
146
+ ## Phase 2 - Grandfather the live installs
147
+
148
+ **This phase runs before Phase 3. It issues licences for plugins that are about to become paid, while
149
+ the files are already in place, so nothing changes on any site.**
150
+
151
+ **Done 2026-09-22.** Exactly one file changed across the whole estate: the licence ledger.
152
+
153
+ - [x] `invoice` licensed on `atlas-learning`, `oneill-leisure`, `jamie-alcock` and `pinpointzero` -
154
+ the four sites where it is **enabled**. 365-day term, 7-day grace, matching the existing
155
+ `proLicenceDays` / `gracePeriodDays` defaults. **Not** `grantPlugin()` - see the trap below.
156
+ - [x] `surveys`' two existing licences (`pinpointzero`, `domma-cms-site`) both point at sites that
157
+ still exist. Left active.
158
+ - [x] Verified: `invoice` still `enabled: true` on all four; no site file written (the only site
159
+ writes in the window were three `analytics-pro/data/hits.jsonl`, which is the visitor tracker
160
+ logging live traffic).
161
+
162
+ **Exit: met.** Ledger now holds 24 records, 4 of them `invoice`, all active.
163
+
164
+ **Rollback:** revoke the four licence ids, or restore
165
+ `data/licences.json.bak-pre-invoice-grandfather-20260922T094837`. Nothing else was touched.
166
+
167
+ ### Trap: `grantPlugin()` would have disabled invoice on four live sites
168
+
169
+ The plan said to use `entitlements.grantPlugin()`. **Do not**, for a plugin that is not resident in
170
+ `paid-plugins/`. `grantPlugin()` issues the licence and then calls
171
+ `pluginDistributor.activate()`, which throws `Plugin "invoice" not found in paid-plugins directory`
172
+ - and its error path calls `pluginDistributor.deactivate()`, writing `enabled: false` into the
173
+ child's `config/plugins.json` before rethrowing. The site keeps serving until its next restart and
174
+ then comes back **without invoicing**, with the cause recorded nowhere near the symptom.
175
+
176
+ Grandfathering was done with `licenceManager.activate()` instead: it writes the ledger record and
177
+ nothing else. No files, no config, no restart. That is the correct tool whenever the files are
178
+ already in place and only the licence is missing.
179
+
180
+ `grantPlugin()` becomes the right call for `invoice` once `paid-plugins/` is a checkout of the
181
+ monorepo - but note that at that point `activate()` will also overwrite each site's copy and minify
182
+ its admin JS, now that `invoice` is `closedSource`. That is a deliberate re-distribution, not part of
183
+ grandfathering.
184
+
185
+ ### Correction to this phase's original exit test
186
+
187
+ It read "every site holding `invoice` **files** also holds an active licence". That is too strict:
188
+ `invoice` files sit on all eight sites because the scaffolder put them there, but only four have it
189
+ enabled. The other four are strays and **Phase 5 deletes them**; licensing a site that does not use
190
+ the plugin would have been wrong. The test is: every site with `invoice` *enabled* holds an active
191
+ licence.
192
+
193
+ ---
194
+
195
+ ## Phase 3 - Extract from the free tier
196
+
197
+ - [x] **`invoice` removed** from `domma-cms/plugins/` (35 files, named-path `git rm`).
198
+ - [x] **`surveys` removed**, with its 11 test files, once option 1 was chosen for the test question.
199
+ - [x] `package.json` had no `files` entry for either. A fresh `npm run build` confirms
200
+ `_publish/plugins/` no longer contains `invoice`.
201
+ - [x] `tier: "paid"` + `closedSource: true` on **both** manager copies. Committed for `invoice`;
202
+ applied on disk for `surveys` but **not committed, because `plugins/surveys/` is gitignored in the
203
+ manager** - see the finding below. The tag still takes effect, since `siteScaffolder` walks the
204
+ filesystem rather than git.
205
+ - [x] History not rewritten; the reasoning is in the commit message.
206
+ - [x] `CHANGELOG.md` entry added under Unreleased.
207
+
208
+ **Exit: met.** `_publish/plugins/` contains neither plugin. The four invoice sites are unaffected and
209
+ still licensed, `surveys` was enabled on no site and its two licences are untouched, and the suite
210
+ passes: **1,584 before, 1,550 after, with the missing 34 now passing in the marketplace repo.**
211
+
212
+ ### Resolved: the tests moved with the plugin (option 1)
213
+
214
+ `tests/surveys/` is **11 files, 527 lines**, and they import from three places at once:
215
+
216
+ ```
217
+ ../../plugins/surveys/lib/{ledger,tokens,campaigns,submit}.js the plugin
218
+ ../../server/services/{collections,forms}.js the CMS core
219
+ fastify, @fastify/jwt domma-cms node_modules
220
+ ```
221
+
222
+ Delete the plugin and `npm test` breaks. Move the tests to the monorepo and they need a CMS checkout
223
+ *and* its `node_modules` to run at all. This is not a `surveys` problem - it is the general question
224
+ of **how a paid plugin's tests run once the plugin no longer lives inside the CMS**, and it will
225
+ recur for every paid plugin that has tests.
226
+
227
+ Three options, none of them free:
228
+
229
+ 1. **Tests move with the plugin, monorepo gains a host-CMS dependency.** Honest about where the code
230
+ lives; costs a `package.json`, an `npm install`, and a documented path to a sibling `domma-cms`
231
+ checkout. Test imports of `../../server/services/…` become the awkward part.
232
+ 2. **Tests stay in `domma-cms`, importing the plugin from the sibling monorepo checkout.** Cheapest
233
+ to do, but `domma-cms`'s suite then fails for anyone without the private repo - including CI.
234
+ 3. **Ship `surveys` to the monorepo without its tests.** Fastest, and the worst: 527 lines of
235
+ coverage for a plugin about to be sold, discarded to avoid a packaging decision.
236
+
237
+ **Chosen: (1).** Tests live at `plugins/<slug>/tests/` in the monorepo and run with `make test`.
238
+
239
+ Implementing it turned up something the options above had not anticipated: **the plugin's own source
240
+ addresses the CMS by relative path too**, not just its tests. `plugins/surveys/plugin.js` imports
241
+ `../../server/services/collections.js`, and `lib/submit.js` requires
242
+ `../../../public/js/form-logic-engine.js` - correct in production, where the plugin is installed at
243
+ `plugins/<slug>/` inside a CMS tree, and broken in a repo whose root has no `server/`.
244
+
245
+ Rewriting those imports would have been the obvious move and the wrong one: it would break every
246
+ plugin where it actually runs. Instead `make link-cms` gives the repo just enough of the CMS's shape,
247
+ as local symlinks into a sibling checkout, none committed:
248
+
249
+ | Link | Points at | Why |
250
+ |---|---|---|
251
+ | `cms` | `../domma-cms` | the host checkout |
252
+ | `node_modules` | `cms/node_modules` | the CMS's own fastify, not a separately pinned one |
253
+ | `server` | `cms/server` | plugin source imports `../../server/services/…` |
254
+ | `public` | `cms/public` | shared browser code, e.g. `public/js/form-logic-engine.js` |
255
+ | `plugins/_lib` | `cms/plugins/_lib` | the shared helper library, imported as `../_lib/…` |
256
+
257
+ Tests also run with cwd set to the CMS root, because the core reads `config/server.json` relative to
258
+ cwd when imported. This harness is generic - it is what every paid plugin's tests will use.
259
+
260
+ The manager's untracked `plugins/surveys/` was deleted at the same time, per the finding below. The
261
+ manager had it disabled, so nothing there changed.
262
+
263
+ ### Finding: `plugins/surveys/` is gitignored in the manager, deliberately
264
+
265
+ ```
266
+ # ── Pro plugins are distributed via paid-plugins/, not tracked in the engine tree ──
267
+ # The 0.32.x domma-cms engine bundles plugins/surveys/; keep it Pro-isolated.
268
+ plugins/surveys/
269
+ ```
270
+
271
+ A past decision had already treated `surveys` as Pro and kept the engine's bundled copy out of the
272
+ manager repo. That is why `paid-plugins/surveys` exists as a separate fork, and why it went stale:
273
+ the engine copy kept being updated in `domma-cms` while the Pro copy sat untouched from 2026-06-29.
274
+ Every other plugin in `domma-cms-manager/plugins/` is tracked normally.
275
+
276
+ Consequence for later phases: once `surveys` does leave `domma-cms`, the manager's untracked
277
+ `plugins/surveys/` should be **deleted**, not kept - it is the engine's bundled copy of a plugin the
278
+ engine no longer ships. `surveys` then lives in `paid-plugins/` only, refreshed from the monorepo.
279
+
280
+ ---
281
+
282
+ ### Finding for step 5: the manager runs paid plugins itself - RESOLVED
283
+
284
+ `domma-cms-manager` is a CMS, and its own `config/plugins.json` has `docs`, `email-pro`, `garage` and
285
+ `invoice` **enabled** - four `closedSource` plugins, with no licence records, because it is ours.
286
+ `cms.dommajs.org` is in the same position with `docs`, `invoice` and `messages`.
287
+
288
+ Once entitlement verification lands (spec step 5), both would refuse to load them at boot - and on the
289
+ manager that means the fleet's management plane losing half its admin, including the screens used to
290
+ issue the licences that would fix it.
291
+
292
+ **Resolved 2026-09-22: a wildcard entitlement.** The manager is not merely "ours", it is the testbed
293
+ where plugins are written, so per-plugin self-issued licences would mean licensing a plugin before
294
+ you can run your own new code. Full reasoning in the spec's
295
+ [Owner instances](../2026-09-22-dcms-marketplace-design.md#owner-instances).
296
+
297
+ ## Phase 4 - Stop seeding paid plugins
298
+
299
+ **Done 2026-09-22.**
300
+
301
+ - [x] `SKIP_PLUGINS` replaced with `if (manifest.tier === 'paid') continue;`, keeping the
302
+ manager-only exclusions and the `_`-prefix skip.
303
+ - [x] Manifest now read from the **source** before the copy, so a paid plugin is never written to the
304
+ site at all.
305
+ - [x] **No readable manifest now means skip** - an addition, not in the original task.
306
+ `discoverPlugins` would not load such a directory either, and this is what keeps `form-builder`
307
+ residue off new sites.
308
+ - [x] `tier: "paid"` added to the manager's `calendar`, `docs`, `email-pro`, `garage`, `messages`.
309
+ - [x] Throwaway site scaffolded: **9 free plugins seeded, 0 paid**, and no `form-builder`.
310
+ - [x] `data-transfer` granted to it and appeared correctly, enabled in its `config/plugins.json`.
311
+ - [x] Throwaway deleted. `scaffoldSite()` writes only into `sites/<slug>` - it never touches
312
+ `sites.json`, `ports.json`, Apache or `processManager` - so nothing was registered or started and
313
+ deleting the directory orphaned nothing. Ledger still 24 records, sites still 9.
314
+ - [x] 67 site-manager service tests pass.
315
+
316
+ **Exit: met.**
317
+
318
+ ### Why only the paid manifests were tagged
319
+
320
+ Absent `tier` means free, matching the CMS loader, so only paid plugins strictly need tagging. Tagging
321
+ the free ones in the manager would also be pointless: the engine updater refreshes those copies from
322
+ `domma-cms`, whose manifests carry no `tier` until `sync-free-tier` lands in step 3, so the tag would
323
+ be overwritten. The paid ones are manager-owned and the updater leaves them alone.
324
+
325
+ ### Rule established: closedSource lives on distribution copies, not local installs
326
+
327
+ `closedSource` was **removed** from the manager's `invoice` copy, reversing part of Phase 3.
328
+ `domma-cms-manager/plugins/` holds the manager's *own installed* plugins; `paid-plugins/` is the
329
+ distribution source. On a local install `closedSource` achieves nothing except stopping us editing
330
+ our own code in the admin editor. The copies that reach customers - monorepo and `paid-plugins/` -
331
+ carry it, which is where it does its job.
332
+
333
+ ### Aside: the manager's `npm test` runs nothing
334
+
335
+ `package.json` globs `tests/**/*.test.js`, but the manager's tests live at
336
+ `plugins/site-manager/services/*.test.js`. `npm test` reports "tests 0" and exits 0, which is worse
337
+ than failing. Run directly, all 67 pass. Not fixed here - it is unrelated to this plan - but it means
338
+ any verification step that says "run the manager's tests" has been passing vacuously.
339
+
340
+ ---
341
+
342
+ ## Phase 5 - Remove the strays
343
+
344
+ Six paid slugs currently sit unlicensed on existing sites, seeded by the drifted skip-list. Left
345
+ alone they will report `"unlicensed"` in the customer's admin once entitlement verification lands.
346
+
347
+ **Done 2026-09-22.** Note it is **nine** sites, not eight: `domma-cms-site` lives under the manager
348
+ rather than in `dcms/`, and earlier audits in this plan had missed it.
349
+
350
+ - [x] Audited all nine sites against the licence ledger.
351
+ - [x] **43 directories removed**, every one disabled and unlicensed, with their `config/plugins.json`
352
+ entries dropped. A last-second guard in the removal script re-checked `enabled` per directory and
353
+ would have refused any that were.
354
+ - [x] `form-builder` gone from all nine; `plugins/site-search/` gone from `domma-cms` (it was
355
+ untracked - git does not track empty directories - so there is nothing to commit for it).
356
+ - [x] `reserved.json` already carried `form-builder` and `site-search` from Phase 1.
357
+ - [x] **No restarts needed.** Everything removed was disabled, so no running process had it loaded.
358
+ All nine domains verified returning 200 afterwards.
359
+
360
+ **Exit: met, with five exceptions held back deliberately** - see below.
361
+
362
+ ### Everything was archived first
363
+
364
+ `domma-cms-manager/var/phase5-removed-plugins-20260922T103005.tar.gz` - 43 directories, 1,280 files,
365
+ 947K, symlinks dereferenced so the real content is inside. Removal is fully reversible from it.
366
+
367
+ This was not in the original task and should have been: `docs` and `form-builder` carry `data/` on
368
+ every site. Most of it turned out to be seeded defaults - the same document UUIDs and identical file
369
+ sizes repeated across sites - but `oneill-leisure/form-builder` held five form definitions that exist
370
+ nowhere else, neither in `content/forms/` nor anywhere code can reach, since the plugin has no code
371
+ left. Dead data, but a customer's dead data, and not something to delete on a judgement call.
372
+
373
+ ### STOP: five plugins are enabled and unlicensed
374
+
375
+ Not removed, per this phase's own rule. These are plugins someone is using:
376
+
377
+ | Site | Plugin | Status |
378
+ |---|---|---|
379
+ | `ginga-ninja` | `messages` | **Licensed 2026-09-22** - was a customer running a paid plugin nobody had billed for |
380
+ | `oneill-leisure` | `messages` | **Licensed 2026-09-22** - as above |
381
+ | `domma-cms-site` | `docs`, `invoice`, `messages` | **Still open** - our own site, the self-licensing question |
382
+
383
+ The two customer cases got the `invoice` treatment: `licenceManager.activate()`, 365-day term, 7-day
384
+ grace, files untouched and no restart, so nothing changed for either customer except that what they
385
+ are using is now on the books. `messages` is not in `paid-plugins/` either, so `grantPlugin()` would
386
+ have hit the same trap it would have hit for `invoice`.
387
+
388
+ Ledger is now **26 records**. The only unlicensed paid plugins left anywhere are the three on
389
+ `domma-cms-site`, which is the same question as the manager running its own paid plugins (see the
390
+ step 5 finding above) - exempt, or self-issue. It is ours, so nothing is at risk while it waits.
391
+
392
+ ### Left uncommitted deliberately
393
+
394
+ The removals show as `D` deletions in each site's own git repo, and for `domma-cms-site` inside the
395
+ manager repo. They are **not committed**: those working trees already carry 70 to 220 files of
396
+ unrelated drift that is not mine to commit, and `config/plugins.json` in particular may hold someone
397
+ else's pending change. Worth committing per site, but as a deliberate act rather than swept up here.
398
+
399
+ ---
400
+
401
+ ## Phase 6 - Dispose of the site-local plugins
402
+
403
+ Four of the five are already tracked in their own sites' git repos, so this phase is disposition
404
+ rather than rescue. None go in the monorepo.
405
+
406
+ **Done 2026-09-22.** A third correction to the version-control claim first: **`shopping-cart` is
407
+ tracked too.** `domma-cms-site` has no `.git` of its own, but the manager repo tracks all 619 of its
408
+ files, those 5 included. **Every plugin on the estate is versioned somewhere.** Each time this was
409
+ got wrong the mistake was the same - looking for a `.git` or a specific home, and not checking
410
+ whether an enclosing repo tracked it.
411
+
412
+ - [x] `reactive-showcase` - **no action required.** 21/21 files tracked in the `reactive` repo. Log a
413
+ separate, optional task to stop vendoring `public/domma-reactive.esm.js` (7,999 of its 9,587 lines)
414
+ and take the library from npm instead.
415
+ - [ ] `retro64-games` - **no action required.** Tracked in `retro-64`. It is structurally a proper
416
+ plugin (four routes, admin view, per-game JSON store) and stays one; it is simply never a
417
+ marketplace plugin.
418
+ - [x] `retro64-cors` - **core feature landed**: `cors.publicPrefixes` in `config/server.json`, with
419
+ `server/services/publicCors.js`, 5 tests, and the hook wired in `server.js`. `publicPrefixes` is
420
+ stripped before the `@fastify/cors` register call rather than passed as an unknown option, and the
421
+ `skip-override` trick is not needed in core because the hook attaches to the root instance.
422
+ **The plugin is NOT deleted yet**: the feature has to reach `retro-64` in a CMS release first, or
423
+ there would be a window with neither. Deleting it, and setting
424
+ `cors.publicPrefixes: ["/public/retro64/"]` on that site, belongs to that release.
425
+ - [ ] `cache-autoflush` - promote into core. **Own task, not this plan**, unchanged (see the spec's
426
+ [Promoting a plugin into core](../2026-09-22-dcms-marketplace-design.md#promoting-a-plugin-into-core)):
427
+ it needs a `config/cache.json` block, a startup hook in `server.js`, and a retirement migration
428
+ modelled on `search-migration.js`. Until then it stays where it is and keeps working.
429
+ - [x] `shopping-cart` - **untouched scaffolder output**: every route commented out, `onEnable` empty.
430
+ Reserved in `reserved.json`, nothing imported. Free is the right tier once something exists.
431
+ - [x] `domma-cms-site` needs nothing - it is tracked in the manager repo, as above.
432
+
433
+ **Exit: met**, except `retro64-cors`'s plugin deletion, which waits on a release.
434
+
435
+ ---
436
+
437
+ ## Phase 7 - Withdraw seo-pro
438
+
439
+ **Order is load-bearing: the bundle first, then the licences.**
440
+
441
+ **Done 2026-09-22**, in the stated order.
442
+
443
+ - [x] `seo-pro` removed from `proBundle` **first**. Confirmed no `settings.proBundle` override exists
444
+ in the manager's `config/plugins.json`, so the default is the whole answer.
445
+ - [x] Three licences revoked. `licenceManager.list({pluginName: 'seo-pro'})` returns three `revoked`
446
+ records and nothing else. Ledger still 26 records.
447
+ - [x] Files deleted from `pinpointzero`, `retro-64`, `atlas-learning` and from `paid-plugins/`,
448
+ archived first to `var/phase7-seo-pro-20260922T104908.tar.gz`.
449
+ - [x] `seo-pro` pruned from `proPlugins` on all three sites, so the PRO badge stays honest.
450
+ - [x] Already reserved in `reserved.json`, and never imported into the monorepo.
451
+
452
+ **Exit: met.** All four affected domains verified 200 afterwards.
453
+
454
+ ### `rescindPlugin()` was not used, and no restart was forced
455
+
456
+ `rescindPlugin()` restarts the child through `processManager`. **That only works from inside the
457
+ manager's own process**, which forks and supervises each site: a `processManager.start()` called from
458
+ a standalone script would parent the site to that script and orphan it on exit. Restarting a fleet
459
+ site has to go through the Site Manager API, not a node one-liner.
460
+
461
+ It turned out not to be needed. Nothing on any site referenced `/api/plugins/seo-pro/meta`, and
462
+ removing a plugin's files does not unregister its route from a process that already loaded it - so the
463
+ plugin simply stops existing at each site's next restart, whenever that happens. `registerPlugins`
464
+ iterates discovered manifests, so a stale `enabled` entry for a missing plugin is ignored rather than
465
+ an error; the config entries were dropped anyway.
466
+
467
+ ---
468
+
469
+ ## Phase 8 - Retire the hand-maintained lists
470
+
471
+ **Done 2026-09-22.**
472
+
473
+ - [x] `KNOWN_HUES` deleted. Every hue is derived from the plugin name, which is what the fallback did
474
+ anyway. `_template` stays colourless.
475
+ - [x] `README.md`'s Bundled Plugins table regenerated from the manifests actually present - the eight
476
+ free-tier plugins - with a pointer to the marketplace for the rest.
477
+ - [x] Already recorded in the spec that `proBundle` moves into `plugin-terms.json` at step 5.
478
+
479
+ **Exit: met.** The only plugin list left in `domma-cms` is `CORE_PLUGINS`, which is a mechanism rather
480
+ than an inventory. Suite at **1,555 passing**.
481
+
482
+ ---
483
+
484
+ ## Verification
485
+
486
+ Run after every phase, not only at the end. The fleet is live throughout.
487
+
488
+ - [ ] `npm test` in `domma-cms` passes, serially. Restore any polluted config file.
489
+ - [ ] All nine customer domains respond. Apache proxies every one through the manager, so a manager
490
+ fault takes them all out at once.
491
+ - [ ] Each site's admin Plugins screen loads and reports no plugin in a failed-load state
492
+ (`getPluginLoadFailures()`).
493
+ - [ ] `getEntitlements()` reports no orphan state on any site: nothing installed-but-unlicensed,
494
+ nothing licensed-but-missing.
495
+ - [ ] A fresh `npm run build` succeeds and `_publish/plugins/` contains exactly the free tier plus
496
+ `_lib` and `_template`.
497
+ - [ ] `git clone` of the monorepo, then a diff of each plugin against its live deployed copy,
498
+ accounts for every difference as minification.
499
+
500
+ ## Out of scope
501
+
502
+ Everything in spec steps 3 to 6: the Ed25519 key and catalogue generation, the envelope format,
503
+ export and import, entitlement verification, the terms store and the Plugin Terms view, and wiring
504
+ the marketplace client to the manager. This plan only moves code, licences and lists into the shape
505
+ those steps assume.
@@ -0,0 +1,154 @@
1
+ # Marketplace Step 3 - The Signing Key and the Catalogue
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Generate the Ed25519 key `marketplace-1` offline, retire `manager-2026` from the CMS,
6
+ and turn the monorepo's 20 plugins into a signed, publishable catalogue.
7
+
8
+ **Architecture:** `make catalogue` walks `plugins/*/plugin.json` and emits `catalogue.json` plus one
9
+ tarball and detached signature per plugin, all signed with a key that never touches the manager host.
10
+ `terms.json` is a separate file on a separate lifecycle, so a price change is a save rather than a
11
+ release. The CMS gains the new public key and loses the old one.
12
+
13
+ **Spec:** `docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md` (sequencing step 3, and
14
+ [Signing keys](../2026-09-22-dcms-marketplace-design.md#signing-keys))
15
+
16
+ **Prerequisite, already done:** `scripts/entitlement.js` and `scripts/issue-entitlement.js` exist in
17
+ `dcms-marketplace` and are tested. They need only a key to become useful.
18
+
19
+ **Critical project rules** (from `CLAUDE.md` + memory):
20
+
21
+ - The private key is generated on a machine that is **not** the manager host and is never copied onto
22
+ it. This server is the manager host.
23
+ - Server on port **4096** is always running; never start or stop it.
24
+ - `npm test` is serial. `domma-cms` is worked on concurrently - commit early, named paths only.
25
+ - Do not publish to npm as part of this plan.
26
+
27
+ **Ordering constraint:** the key exists before anything is signed, and `manager-2026` leaves
28
+ `TRUSTED_KEYS` in the same release that adds `marketplace-1`. Shipping the new key while keeping the
29
+ old one would leave a window where a bundle signed by a lost key is still accepted.
30
+
31
+ ---
32
+
33
+ ## Phase 1 - Generate and place the key - DONE (on this host, deliberately)
34
+
35
+ **Generated 2026-09-22 on the manager host**, which is a deliberate deviation from this phase's own
36
+ instruction - see the spec's Custody section, which records it rather than pretending otherwise.
37
+
38
+ - [x] Pair generated:
39
+ ```sh
40
+ openssl genpkey -algorithm ed25519 -out marketplace-1.pem
41
+ openssl pkey -in marketplace-1.pem -pubout -out marketplace-1.pub
42
+ ```
43
+ - [x] Private half at `~/.domma-keys/marketplace-1.pem` (dir 0700, key 0600), outside every
44
+ repository.
45
+ - [x] **Second copy held off this disk**, confirmed by the owner 2026-09-22. Recorded as reported
46
+ rather than verified: this host has a single filesystem, no removable media and no network mounts,
47
+ so a backup here is by definition somewhere I cannot see.
48
+
49
+ This was the failure being recovered from - `plugin-signing-2026.pem` was named in the code and
50
+ existed nowhere. The key has now signed a catalogue, so losing it would mean re-signing everything
51
+ under a new keyId and shipping a CMS release before anyone trusted it.
52
+ - [x] `marketplace-1.pub` committed to `dcms-marketplace/keys/`; `*.pem` is gitignored there, and
53
+ `git check-ignore` confirms the private half cannot land by accident.
54
+ - [ ] Record in `PROVENANCE.md` where the private key lives - the location, not the key.
55
+
56
+ **Exit:** `keys/marketplace-1.pub` is committed, the private half is in two safe places, and
57
+ `git log -p` shows no `.pem` anywhere.
58
+
59
+ ## Phase 2 - Swap the key in the CMS - DONE
60
+
61
+ - [x] `marketplace-1` added, `manager-2026` deleted, verifier switched to `crypto.verify(null, …)`.
62
+ - [ ] Delete the `PLACEHOLDER` guard if it no longer applies, or keep it if a placeholder is still
63
+ conceivable. It exists so a stub key can never verify anything.
64
+ - [x] Tests added via a new `trustedKeyIds()` export, so the contract is checkable without key
65
+ material: exactly one key trusted, `manager-2026` gone, the trusted key is **Ed25519** (an RSA
66
+ paste would verify fine and only fail in the field), and `manager-2026` is refused by name before
67
+ any signature check.
68
+ - [ ] Still to do: the signed-bundle install round trip, which needs the catalogue build of Phase 3
69
+ to produce a real bundle.
70
+ - [ ] `npm test` passes.
71
+
72
+ **Exit:** the CMS trusts exactly one key, and the retired one is refused by name.
73
+
74
+ ## Phase 3 - Catalogue generation - DONE
75
+
76
+ - [x] `scripts/build-catalogue.js`, wired to `make catalogue KEY=...`, emitting:
77
+ ```
78
+ dist/catalogue.json signed index
79
+ dist/catalogue.json.sig
80
+ dist/bundles/<slug>-<version>.tar.gz
81
+ dist/bundles/<slug>-<version>.tar.gz.sig
82
+ ```
83
+ - [x] Entries carry **artifact facts only**: `slug`, `version`, `displayName`, `description`, `tier`,
84
+ `icon`, `supersedes`, `requires`, `minCmsVersion`. No prices - those live in `terms.json`.
85
+ - [x] **Refuses any slug in `reserved.json`**, and aborts with `dist/` untouched. `seo-pro`, `shopping-cart`, `form-builder` and
86
+ `site-search` must not be publishable by someone adding a directory and forgetting why the name was
87
+ held. `scripts/verify.js` already fails a reserved slug; the catalogue build must too.
88
+ - [x] Excludes `data/`, `backups/`, `node_modules/` and `CLAUDE.md` from every tarball. `data-transfer` writes backup trees at
89
+ runtime and one site had 28 of them.
90
+ - [x] Tarball format **proven**, not assumed, through `pluginInstaller.extractTar()`: which strips the leading
91
+ path component and ignores symlinks. **Generate a tarball and install it on a scratch CMS** rather
92
+ than assuming the formats agree.
93
+ - [x] Reuses `canonicalise()` for the catalogue signature, so there is one
94
+ canonical-JSON implementation in the repo rather than two that can drift.
95
+
96
+ **Exit: met.** 14 plugins, 2 free and 12 paid. A generated `mongodb-adapter` bundle installed on the
97
+ dev CMS through the real installer and its files landed correctly; a copy with one byte flipped was
98
+ refused with "Plugin signature invalid". The catalogue signature verifies and fails on tampering.
99
+
100
+ ### Found while building: validation order
101
+
102
+ The first version wiped `dist/` and then validated as it went, so **one bad manifest destroyed the
103
+ last good catalogue on its way to failing**. Surfaced by testing the reserved-slug refusal, which is
104
+ precisely the case that would hit it in practice. Validation now runs first and a failed build leaves
105
+ the previous output exactly where it was.
106
+
107
+ Builds are also reproducible - `--sort=name`, fixed mtime, `gzip -n` - so unchanged code produces
108
+ identical signatures. Without it every release would churn every signature and customers would
109
+ re-download code that had not changed.
110
+
111
+ ## Phase 4 - The free-tier sync
112
+
113
+ - [ ] `scripts/sync-free-tier.js`, wired to `make sync-free-tier`: copy every `tier: "free"` plugin
114
+ into `domma-cms/plugins/`, so `npm install domma-cms` stays a complete offline plugin-development
115
+ environment.
116
+ - [ ] It must **not** touch `_lib` or `_template`, which live in `domma-cms` and are its own.
117
+ - [ ] Decide the direction of truth first. Today `domma-cms` is still the working copy for free
118
+ plugins, and this script would make the monorepo authoritative. Whichever is chosen, only one of
119
+ them can be edited afterwards, and the other is generated. **This is open question 5 in the spec
120
+ and should be settled before the script exists**, because a two-way sync is how both copies end up
121
+ wrong.
122
+
123
+ **Exit:** the free subset in `domma-cms` is reproducible from the monorepo by one command, and it is
124
+ documented which side is authoritative.
125
+
126
+ ## Phase 5 - Publish
127
+
128
+ - [ ] Put `dist/` on object storage plus CDN. Static files only: no application server on the public
129
+ path, which is the whole point of signing offline.
130
+ - [ ] `terms.json` publishes alongside, rewritten whenever a price changes. It does **not** require a
131
+ catalogue rebuild.
132
+ - [ ] Verify a plain `curl` of the catalogue and one bundle from an unauthenticated client, since
133
+ that is exactly what a CMS does.
134
+
135
+ **Exit:** a CMS with no credentials can fetch and verify the catalogue and a free bundle.
136
+
137
+ ## Verification
138
+
139
+ - [ ] `npm test` in `domma-cms` passes, serially.
140
+ - [ ] `make verify` and `make test` pass in `dcms-marketplace`.
141
+ - [ ] All nine customer domains respond; the fleet is untouched by any of this.
142
+ - [ ] A bundle signed with a throwaway key is **refused** by a CMS trusting only `marketplace-1`.
143
+ - [ ] `git log -p` across both repos contains no private key material.
144
+
145
+ ## Out of scope
146
+
147
+ Step 4's envelope and export/import, step 5's entitlement verification and terms store, and step 6's
148
+ wiring of the marketplace client. Issuing the owner wildcards is step 5 work and cannot happen until
149
+ the verifier exists - though once the key from Phase 1 exists, the command itself is ready:
150
+
151
+ ```sh
152
+ node scripts/issue-entitlement.js --key <private.pem> --slug '*' --model perpetual --no-support \
153
+ --purchaser-name "Domma (owner instance: dcm.dommajs.org)" --out entitlement.json
154
+ ```