@geekity/cms 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +96 -160
  2. package/admin/pages/documents/editor.njk +48 -0
  3. package/admin/pages/federation/settings.njk +1 -59
  4. package/admin/pages/plugins/installed.njk +113 -0
  5. package/admin/pages/plugins/screen.njk +129 -0
  6. package/admin/static/admin.css +144 -4
  7. package/admin/static/editor-actions.js +202 -0
  8. package/dist/admin/accounts.d.ts +18 -39
  9. package/dist/admin/accounts.d.ts.map +1 -1
  10. package/dist/admin/accounts.js +19 -48
  11. package/dist/admin/accounts.js.map +1 -1
  12. package/dist/admin/documents.d.ts +5 -0
  13. package/dist/admin/documents.d.ts.map +1 -1
  14. package/dist/admin/documents.js +17 -8
  15. package/dist/admin/documents.js.map +1 -1
  16. package/dist/admin/editor-actions.d.ts +35 -0
  17. package/dist/admin/editor-actions.d.ts.map +1 -0
  18. package/dist/admin/editor-actions.js +187 -0
  19. package/dist/admin/editor-actions.js.map +1 -0
  20. package/dist/admin/federation.d.ts +4 -5
  21. package/dist/admin/federation.d.ts.map +1 -1
  22. package/dist/admin/federation.js +4 -5
  23. package/dist/admin/federation.js.map +1 -1
  24. package/dist/admin/index.d.ts +1 -1
  25. package/dist/admin/index.d.ts.map +1 -1
  26. package/dist/admin/index.js +1 -1
  27. package/dist/admin/index.js.map +1 -1
  28. package/dist/admin/menu.d.ts +1 -1
  29. package/dist/admin/menu.d.ts.map +1 -1
  30. package/dist/admin/menu.js +10 -3
  31. package/dist/admin/menu.js.map +1 -1
  32. package/dist/admin/plugins.d.ts +24 -0
  33. package/dist/admin/plugins.d.ts.map +1 -0
  34. package/dist/admin/plugins.js +312 -0
  35. package/dist/admin/plugins.js.map +1 -0
  36. package/dist/admin/routes.d.ts.map +1 -1
  37. package/dist/admin/routes.js +5 -1
  38. package/dist/admin/routes.js.map +1 -1
  39. package/dist/admin/settings-federation.d.ts +0 -25
  40. package/dist/admin/settings-federation.d.ts.map +1 -1
  41. package/dist/admin/settings-federation.js +1 -54
  42. package/dist/admin/settings-federation.js.map +1 -1
  43. package/dist/admin/settings.d.ts +2 -14
  44. package/dist/admin/settings.d.ts.map +1 -1
  45. package/dist/admin/settings.js +1 -21
  46. package/dist/admin/settings.js.map +1 -1
  47. package/dist/admin/store.d.ts +8 -1
  48. package/dist/admin/store.d.ts.map +1 -1
  49. package/dist/admin/store.js +7 -3
  50. package/dist/admin/store.js.map +1 -1
  51. package/dist/admin/templates.d.ts +2 -0
  52. package/dist/admin/templates.d.ts.map +1 -1
  53. package/dist/admin/templates.js +2 -0
  54. package/dist/admin/templates.js.map +1 -1
  55. package/dist/cli.d.ts +2 -3
  56. package/dist/cli.d.ts.map +1 -1
  57. package/dist/cli.js +278 -212
  58. package/dist/cli.js.map +1 -1
  59. package/dist/config.d.ts +20 -0
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +6 -0
  62. package/dist/config.js.map +1 -1
  63. package/dist/content/store.d.ts +6 -0
  64. package/dist/content/store.d.ts.map +1 -1
  65. package/dist/content/store.js +3 -0
  66. package/dist/content/store.js.map +1 -1
  67. package/dist/drain.d.ts +28 -0
  68. package/dist/drain.d.ts.map +1 -0
  69. package/dist/drain.js +106 -0
  70. package/dist/drain.js.map +1 -0
  71. package/dist/env.d.ts +11 -0
  72. package/dist/env.d.ts.map +1 -1
  73. package/dist/federation/federation.d.ts +4 -2
  74. package/dist/federation/federation.d.ts.map +1 -1
  75. package/dist/federation/federation.js +16 -21
  76. package/dist/federation/federation.js.map +1 -1
  77. package/dist/federation/inbox.d.ts +23 -3
  78. package/dist/federation/inbox.d.ts.map +1 -1
  79. package/dist/federation/inbox.js +63 -1
  80. package/dist/federation/inbox.js.map +1 -1
  81. package/dist/federation/index.d.ts +0 -4
  82. package/dist/federation/index.d.ts.map +1 -1
  83. package/dist/federation/index.js +0 -2
  84. package/dist/federation/index.js.map +1 -1
  85. package/dist/federation/mount.d.ts +6 -12
  86. package/dist/federation/mount.d.ts.map +1 -1
  87. package/dist/federation/mount.js +3 -50
  88. package/dist/federation/mount.js.map +1 -1
  89. package/dist/federation/queue.d.ts +22 -0
  90. package/dist/federation/queue.d.ts.map +1 -0
  91. package/dist/federation/queue.js +58 -0
  92. package/dist/federation/queue.js.map +1 -0
  93. package/dist/files/atomic.d.ts +13 -0
  94. package/dist/files/atomic.d.ts.map +1 -1
  95. package/dist/files/atomic.js +45 -0
  96. package/dist/files/atomic.js.map +1 -1
  97. package/dist/index.d.ts +30 -3
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +156 -79
  100. package/dist/index.js.map +1 -1
  101. package/dist/plugin.d.ts +524 -0
  102. package/dist/plugin.d.ts.map +1 -0
  103. package/dist/plugin.js +23 -0
  104. package/dist/plugin.js.map +1 -0
  105. package/dist/plugins/enabled.d.ts +23 -0
  106. package/dist/plugins/enabled.d.ts.map +1 -0
  107. package/dist/plugins/enabled.js +49 -0
  108. package/dist/plugins/enabled.js.map +1 -0
  109. package/dist/plugins/federation.d.ts +26 -0
  110. package/dist/plugins/federation.d.ts.map +1 -0
  111. package/dist/plugins/federation.js +114 -0
  112. package/dist/plugins/federation.js.map +1 -0
  113. package/dist/plugins/fetch.d.ts +15 -0
  114. package/dist/plugins/fetch.d.ts.map +1 -0
  115. package/dist/plugins/fetch.js +33 -0
  116. package/dist/plugins/fetch.js.map +1 -0
  117. package/dist/plugins/folder.d.ts +50 -0
  118. package/dist/plugins/folder.d.ts.map +1 -0
  119. package/dist/plugins/folder.js +171 -0
  120. package/dist/plugins/folder.js.map +1 -0
  121. package/dist/plugins/install.d.ts +45 -0
  122. package/dist/plugins/install.d.ts.map +1 -0
  123. package/dist/plugins/install.js +275 -0
  124. package/dist/plugins/install.js.map +1 -0
  125. package/dist/plugins/mount.d.ts +17 -0
  126. package/dist/plugins/mount.d.ts.map +1 -0
  127. package/dist/plugins/mount.js +44 -0
  128. package/dist/plugins/mount.js.map +1 -0
  129. package/dist/plugins/registry.d.ts +110 -0
  130. package/dist/plugins/registry.d.ts.map +1 -0
  131. package/dist/plugins/registry.js +447 -0
  132. package/dist/plugins/registry.js.map +1 -0
  133. package/dist/plugins/settings.d.ts +64 -0
  134. package/dist/plugins/settings.d.ts.map +1 -0
  135. package/dist/plugins/settings.js +219 -0
  136. package/dist/plugins/settings.js.map +1 -0
  137. package/dist/plugins/site.d.ts +17 -0
  138. package/dist/plugins/site.d.ts.map +1 -0
  139. package/dist/plugins/site.js +67 -0
  140. package/dist/plugins/site.js.map +1 -0
  141. package/dist/supervisor/primary.d.ts +47 -0
  142. package/dist/supervisor/primary.d.ts.map +1 -0
  143. package/dist/supervisor/primary.js +208 -0
  144. package/dist/supervisor/primary.js.map +1 -0
  145. package/dist/supervisor/protocol.d.ts +37 -0
  146. package/dist/supervisor/protocol.d.ts.map +1 -0
  147. package/dist/supervisor/protocol.js +29 -0
  148. package/dist/supervisor/protocol.js.map +1 -0
  149. package/dist/supervisor/supervision.d.ts +25 -0
  150. package/dist/supervisor/supervision.d.ts.map +1 -0
  151. package/dist/supervisor/supervision.js +7 -0
  152. package/dist/supervisor/supervision.js.map +1 -0
  153. package/dist/supervisor/worker.d.ts +29 -0
  154. package/dist/supervisor/worker.d.ts.map +1 -0
  155. package/dist/supervisor/worker.js +97 -0
  156. package/dist/supervisor/worker.js.map +1 -0
  157. package/dist/web/maintenance.d.ts +2 -2
  158. package/dist/web/maintenance.js +2 -2
  159. package/dist/web/negotiate.d.ts +4 -3
  160. package/dist/web/negotiate.d.ts.map +1 -1
  161. package/dist/web/negotiate.js +4 -3
  162. package/dist/web/negotiate.js.map +1 -1
  163. package/dist/web/routes.d.ts.map +1 -1
  164. package/dist/web/routes.js +33 -14
  165. package/dist/web/routes.js.map +1 -1
  166. package/package.json +10 -1
  167. package/dist/comments/conversation.d.ts +0 -29
  168. package/dist/comments/conversation.d.ts.map +0 -1
  169. package/dist/comments/conversation.js +0 -60
  170. package/dist/comments/conversation.js.map +0 -1
  171. package/dist/federation/import-wordpress.d.ts +0 -128
  172. package/dist/federation/import-wordpress.d.ts.map +0 -1
  173. package/dist/federation/import-wordpress.js +0 -426
  174. package/dist/federation/import-wordpress.js.map +0 -1
  175. package/dist/federation/wordpress.d.ts +0 -131
  176. package/dist/federation/wordpress.d.ts.map +0 -1
  177. package/dist/federation/wordpress.js +0 -342
  178. package/dist/federation/wordpress.js.map +0 -1
  179. package/dist/web/comments.d.ts +0 -60
  180. package/dist/web/comments.d.ts.map +0 -1
  181. package/dist/web/comments.js +0 -207
  182. package/dist/web/comments.js.map +0 -1
package/README.md CHANGED
@@ -86,21 +86,26 @@ message on a site whose `site.json` has not set a period.
86
86
  site (or `npx geekity`, or a `package.json` script, which is how the generated
87
87
  `sync` script calls it).
88
88
 
89
- | Command | What it does |
90
- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
- | `geekity serve` | Boot from the config file and listen. The default when no command is given. |
92
- | `geekity init <dir>` | Create a new site in `<dir>`. Refuses a directory that is not empty. |
93
- | `geekity sync` | Rebuild the content index once and exit. Exits non-zero if any file could not be parsed. |
94
- | `geekity rebuild` | Delete `data/geekity.db` and build it again from the files. |
95
- | `geekity resend --all`, `<slug>...` | Send announced posts to every follower and relay again, as they now read. See [Quote posts](#quote-posts). |
96
- | `geekity maintenance on`, `off`, `status` | Take the public site down on purpose with a 503 and `Retry-After`, or bring it back, without a restart. `on --until <time>` names when it should be back. |
97
- | `geekity strip-metadata` | Remove location and camera metadata from files already in `content/uploads`. See [The media library](#the-media-library). |
98
- | `geekity user add <name>` | Create an admin account, so a site can get its first login without the setup screen. |
99
- | `geekity import wordpress-actor <name>` | Bring one person across from the WordPress ActivityPub plugin: their key pair, the actor id their followers hold, the plugin's numeric actor id, and their followers. |
100
- | `geekity --help`, `-h` | The same table, on the terminal. |
101
- | `geekity --version` | The installed version. |
102
-
103
- `serve`, `sync`, `rebuild`, `resend`, `maintenance`, `strip-metadata`, `user add` and `import wordpress-actor` take
89
+ | Command | What it does |
90
+ | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `geekity serve` | Boot from the config file and listen, in a worker the command supervises: Reload on the Plugins screen replaces it, and a crash respawns it. The default when no command is given. |
92
+ | `geekity init <dir>` | Create a new site in `<dir>`. Refuses a directory that is not empty. |
93
+ | `geekity sync` | Rebuild the content index once and exit. Exits non-zero if any file could not be parsed. |
94
+ | `geekity rebuild` | Delete `data/geekity.db` and build it again from the files. |
95
+ | `geekity resend --all`, `<slug>...` | Send announced posts to every follower and relay again, as they now read. See [Quote posts](#quote-posts). |
96
+ | `geekity maintenance on`, `off`, `status` | Take the public site down on purpose with a 503 and `Retry-After`, or bring it back, without a restart. `on --until <time>` names when it should be back. |
97
+ | `geekity strip-metadata` | Remove location and camera metadata from files already in `content/uploads`. See [The media library](#the-media-library). |
98
+ | `geekity user add <name>` | Create an admin account, so a site can get its first login without the setup screen. |
99
+ | `geekity plugin add <package>`, `remove` | Install a plugin package's bundle from the npm registry into the plugins folder (`GEEKITY_PLUGINS_DIR`), or delete it. Reload on the Plugins screen loads the change. |
100
+ | `geekity --help`, `-h` | The same table, on the terminal. |
101
+ | `geekity --version` | The installed version. |
102
+
103
+ An installed plugin may add commands of its own, which `geekity --help` lists
104
+ under _Plugin commands_ and which run whether or not the plugin is enabled.
105
+ That includes a plugin in the plugins folder. The repository README's
106
+ _Plugins_ section describes `geekity plugin add` and writing a plugin.
107
+
108
+ `serve`, `sync`, `rebuild`, `resend`, `maintenance`, `strip-metadata`, `user add`, `plugin` and every plugin command take
104
109
  `--config <file>`; without it they look for `geekity.config.ts`, then
105
110
  `geekity.config.js`, then `geekity.config.mjs` in the working directory, and run
106
111
  on defaults if there is none.
@@ -291,9 +296,28 @@ directory; absolute ones are used as given.
291
296
  | `federation` | `{}` | — | Federation stores and guards. See [Federation](#federation). |
292
297
  | `commentChecker` | Akismet | — | A spam checker of the site's own, which wins over the key in `data/akismet.json`. See [Akismet](#akismet). |
293
298
  | `mail` | `{}` | — | Mail provider, retries, backoff and logger. See [Email](#email). |
299
+ | `plugins` | `[]` | — | The installed plugins. Which run is the `plugins` key of `site.json`, set under Admin > Plugins. A plugin's secret settings take an environment variable each; see below. |
294
300
 
295
301
  Precedence is environment variable, then config file, then default, so a host
296
- can override anything without editing the site. A boolean environment variable
302
+ can override anything without editing the site.
303
+
304
+ A plugin's secret settings, such as an API key, can come from the environment
305
+ too, which is how a Docker site keeps a key in `.env`. The variable is the
306
+ plugin's package name, then `__`, then the setting's key, each upper-cased with
307
+ every run of other characters, underscores included, written as one `_`:
308
+
309
+ | Package | Setting | Variable |
310
+ | --------------------- | --------- | ----------------------------- |
311
+ | `@geekity/plugin-llm` | `api_key` | `GEEKITY_PLUGIN_LLM__API_KEY` |
312
+ | `@acme/plugin-llm` | `api_key` | `ACME_PLUGIN_LLM__API_KEY` |
313
+
314
+ A set variable wins over the value stored in
315
+ `data/plugins/<package name>/secrets.json`, and the plugin's screen shows the
316
+ setting as set by the environment, with no box to change it. The screen prints
317
+ each secret's exact variable name. Only plugin variables contain `__`, so none
318
+ can shadow a core one. Two installed packages whose names convert to the same
319
+ prefix, such as `@a/b-c` and `@a-b/c`, both refuse to load, and Admin > Plugins
320
+ names the two. A boolean environment variable
297
321
  takes `true`, `1`, `yes` and `on`, or their opposites; anything else is an error
298
322
  rather than a silent `false`.
299
323
 
@@ -361,20 +385,20 @@ the same directory reads all of it, and everything in it is meant to be public:
361
385
 
362
386
  `data/` is private. It is gitignored, and it is the half to copy somewhere safe:
363
387
 
364
- | Path | What it holds |
365
- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
366
- | `data/users.json` | Usernames and argon2id password hashes. Mode `0600`. |
367
- | `data/keys/` | Each user's actor key pairs as JWK files. Mode `0600`. **Losing these breaks federation.** |
368
- | `data/locations.json` | Where each post was written, keyed by permalink (decision-29). Mode `0600`. |
369
- | `data/comment-salt` | What hides commenters' addresses in the published comment files. Mode `0600`. |
370
- | `data/comments/{slug}.json` | The emails of that post's commenters, and whether each asked to hear about replies, keyed by comment id. Mode `0600`. |
371
- | `data/akismet.json` | The Akismet key, and what `verify-key` last said about it. Mode `0600`. |
372
- | `data/mail.json` | The mail credential: a Brevo API key, an SMTP connection, or both. Mode `0600`. |
373
- | `data/notification-secret` | What signs the one-click links in a notification. Mode `0600`. Losing it kills every link already in an inbox and nothing else. |
374
- | `data/comment-optouts.json` | The addresses that have unsubscribed from reply notices. Mode `0600`. |
375
- | `data/indieauth-tokens.json` | A SHA-256 hash of each IndieAuth access and refresh token, with its user, app, scopes and expiry (decision-24). Mode `0600`. |
376
- | `data/notification-digests.json` | When each user was last sent a digest. Mode `0600`. Losing it sends one digest early and nothing worse. |
377
- | `data/wordpress-activitypub.json` | When each WordPress compatibility path was last asked for. Losing it resets the answer the switch is watched by. |
388
+ | Path | What it holds |
389
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
390
+ | `data/users.json` | Usernames and argon2id password hashes. Mode `0600`. |
391
+ | `data/keys/` | Each user's actor key pairs as JWK files. Mode `0600`. **Losing these breaks federation.** |
392
+ | `data/locations.json` | Where each post was written, keyed by permalink (decision-29). Mode `0600`. |
393
+ | `data/comment-salt` | What hides commenters' addresses in the published comment files. Mode `0600`. |
394
+ | `data/comments/{slug}.json` | The emails of that post's commenters, and whether each asked to hear about replies, keyed by comment id. Mode `0600`. |
395
+ | `data/akismet.json` | The Akismet key, and what `verify-key` last said about it. Mode `0600`. |
396
+ | `data/mail.json` | The mail credential: a Brevo API key, an SMTP connection, or both. Mode `0600`. |
397
+ | `data/notification-secret` | What signs the one-click links in a notification. Mode `0600`. Losing it kills every link already in an inbox and nothing else. |
398
+ | `data/comment-optouts.json` | The addresses that have unsubscribed from reply notices. Mode `0600`. |
399
+ | `data/indieauth-tokens.json` | A SHA-256 hash of each IndieAuth access and refresh token, with its user, app, scopes and expiry (decision-24). Mode `0600`. |
400
+ | `data/notification-digests.json` | When each user was last sent a digest. Mode `0600`. Losing it sends one digest early and nothing worse. |
401
+ | `data/plugins/<package name>/` | Each plugin's private folder. Its secret settings are in `secrets.json`, mode `0600`; the rest is the plugin's to say. |
378
402
 
379
403
  Three things under `data/` may be deleted whenever the site is stopped, and
380
404
  nothing else in either directory may:
@@ -617,7 +641,8 @@ hand. Pages, drafts, scheduled and trashed posts do not federate, so nobody
617
641
  can quote them.
618
642
 
619
643
  A quoting server sends a `QuoteRequest` to the post's author. The inbox, and
620
- the WordPress-compatible inbox when that switch is on, answers it:
644
+ any plugin inbox that hands its deliveries to the site's own handlers, answers
645
+ it:
621
646
 
622
647
  - For a post the site federates, whose quote lives on the requester's own
623
648
  server, it stores an approval and replies `Accept` with a
@@ -756,135 +781,48 @@ actor lists it in `alsoKnownAs` beside the archive and `/@{username}`. Nothing
756
781
  else moves: `url` is still the archive and the collections are still the
757
782
  archive's children, because a peer refetches those.
758
783
 
759
- The CMS never mints one and no screen writes one. It arrives with the WordPress
760
- import, or is typed into the file by hand; `/admin/users/<id>` shows it
784
+ The CMS never mints one and no screen writes one. It arrives with a plugin's
785
+ import, such as the WordPress plugin's, or is typed into the file by hand; `/admin/users/<id>` shows it
761
786
  read-only beside the account, and a value that is not an absolute URL is
762
787
  ignored.
763
788
 
764
- ### WordPress ActivityPub compatibility
789
+ ### Moving a site from the WordPress ActivityPub plugin
765
790
 
766
- A site that moved here from the WordPress ActivityPub plugin has followers
767
- whose servers still hold the plugin's endpoints — `/wp-json/activitypub/1.0/actors/2/inbox`,
768
- the shared `/wp-json/activitypub/1.0/inbox`, and the collections beside them.
769
- Those are cache rather than identity: a follower's server replaces them the
770
- next time it refetches the actor. So the CMS can carry them for a while, behind
771
- a switch, and is meant to stop.
772
-
773
- Turn **WordPress ActivityPub compatibility** on under
774
- `/admin/federation/settings`. It is off by default and `site.json` says nothing
775
- about it until it is on. It needs one thing on the user record in
776
- `data/users.json` besides the stored actor id above: the number WordPress gave
777
- that person, which is what its paths are built from.
778
-
779
- ```json
780
- {
781
- "id": 2,
782
- "username": "ada",
783
- "actorId": "https://example.com/?author=2",
784
- "wordpressActorId": 2
785
- }
786
- ```
787
-
788
- `geekity import wordpress-actor` writes both; no screen does. With the switch
789
- on, those paths are real inbox routes, signature-verified exactly as the
790
- canonical ones are — an unsigned or badly signed delivery is refused — plus GET
791
- routes for the actor, its outbox, its followers and its following. What a peer
792
- reads back is always the canonical document: the same `id`, the same key, and
793
- the _canonical_ inbox and collections, so a follower refetching the actor at
794
- the old URL is the follower that learns the new endpoints. A user with no
795
- `wordpressActorId` is not reachable through any of it.
796
-
797
- Every one of those paths records the instant it was last asked for, per user,
798
- in `data/wordpress-activitypub.json` — a file, so a deleted database does not
799
- forget it — and the settings page lists each path beside the switch with that
800
- instant, or _Never_. That is what the switch is watched by: once every
801
- follower's server has refetched the actor, nothing asks any more, and the
802
- switch can go off. Clearing it takes the paths away on the very next request,
803
- with no restart.
804
-
805
- ### Moving a site off the WordPress ActivityPub plugin
806
-
807
- `geekity import wordpress-actor` brings one person across. It writes the three
808
- things the CMS cannot mint for itself — the RSA key pair the followers have
809
- cached, the actor id they key the account by, and the followers — and it is a
810
- command rather than a screen because a stored actor id is identity for the life
811
- of the account.
812
-
813
- ```sh
814
- geekity import wordpress-actor ada \
815
- --actor-id 'https://example.com/?author=2' \
816
- --wordpress-id 2 \
817
- --keypair ada.keypair.json
818
- ```
819
-
820
- It writes `data/keys/ada.rsassa-pkcs1-v1_5.jwk` (and mints the Ed25519 pair
821
- beside it, which WordPress never had), puts `actorId` and `wordpressActorId` on
822
- the user in `data/users.json`, and fetches the plugin's public followers
823
- collection into `content/_data/federation/ada/followers.json`, dereferencing
824
- each follower for its inbox, shared inbox, handle, name, avatar and profile URL.
825
-
826
- | Option | What it is |
827
- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
828
- | `--actor-id <url>` | The id WordPress published, query string and all. The one thing the import cannot work out for itself. |
829
- | `--wordpress-id <n>` | The WordPress user id, which is the number in the plugin's paths. |
830
- | `--keypair <file>` | The JSON the plugin's option holds: `{"private_key": …, "public_key": …}`. |
831
- | `--private-key <file>`, `--public-key <file>` | The two PEMs instead, for a site still on the legacy user meta. The public key is only checked against the private half; it is derived, never stored. |
832
- | `--followers <url\|file\|none>` | Where the followers come from. Left off, the plugin's own collection on the actor id's origin. |
833
- | `--force` | Import over a key pair the user already has. |
834
-
835
- **Exporting the key pair with wp-cli.** The plugin keeps a user's pair in a
836
- WordPress option named after their login:
837
-
838
- ```sh
839
- # On the WordPress server. The login, not the user id, is what names the option.
840
- wp option get activitypub_keypair_for_ada --format=json > ada.keypair.json
841
- ```
842
-
843
- A site old enough to still be on the plugin's legacy storage has the pair in
844
- user meta instead, one PEM per key:
845
-
846
- ```sh
847
- wp user meta get 2 magic_sig_private_key > ada.private.pem
848
- wp user meta get 2 magic_sig_public_key > ada.public.pem
849
- ```
850
-
851
- Either export is accepted, and either PEM encoding — `-----BEGIN PRIVATE
852
- KEY-----` (PKCS#8) or `-----BEGIN RSA PRIVATE KEY-----` (PKCS#1). Treat the
853
- files the way you would treat a password: the private key is the account.
854
-
855
- **Running it twice changes nothing**, and says so, which is what makes it safe
856
- to run again after an instance that was down comes back — a follower already in
857
- the file keeps its place and its follow time. A follower whose server will not
858
- answer is listed and skipped rather than failing the import. A user who already
859
- has a _different_ key pair is refused: importing over one would change that
860
- person's identity, and every follower has cached the public half of the key
861
- that is there. `--force` is the way past that, and is only right when you are
862
- certain the pair being imported is the one the followers hold.
791
+ A site that moved here from the WordPress ActivityPub plugin keeps its people's
792
+ actor ids and its posts' object ids, which are identity and are the CMS's own
793
+ (above). What it needs for a while besides is the plugin's old paths, which
794
+ followers' servers keep delivering to until they refetch the actor, and an
795
+ import that brings each person's key pair and followers across. Both are the
796
+ [`@geekity/plugin-wordpress`](../plugin-wordpress/README.md) package, which a
797
+ site born on the CMS never installs. decision-14's switch is that plugin now:
798
+ where the cutover used to say "turn the switch on", it says install and enable
799
+ the WordPress plugin.
863
800
 
864
801
  #### The cutover, end to end
865
802
 
866
- 1. **Export, while the WordPress site is still up.** The key pair, as above.
867
- The followers can be saved too — `curl -H 'Accept: application/activity+json'
868
- 'https://example.com/wp-json/activitypub/1.0/actors/2/followers?page=1' >
869
- followers.json` — which is worth doing if the old site is going away before
870
- the import runs. Note the actor id and the numeric actor id off
803
+ 1. **Export, while the WordPress site is still up.** Each person's key pair,
804
+ and the followers if the old site is going away before the import runs. The
805
+ plugin's README says how. Note the actor id and the numeric actor id off
871
806
  `https://example.com/?author=2`, and bring the content across.
872
- 2. **Import.** `geekity import wordpress-actor ada --actor-id … --wordpress-id
873
- … --keypair ada.keypair.json`, pointing `--followers` at the saved file if
874
- you took one. Check the report: every follower should be added, and any that
875
- were skipped should be re-run once their servers answer.
876
- 3. **Switch on.** Turn **WordPress ActivityPub compatibility** on under
877
- `/admin/federation/settings`, then move the DNS. Followers' servers go on
878
- delivering to the plugin's old inbox paths until they next refetch the
879
- actor, and the switch is what catches those deliveries.
807
+ 2. **Install the WordPress plugin and import.** Add `@geekity/plugin-wordpress`
808
+ to the site's dependencies and to `plugins` in its config, then run `geekity import
809
+ wordpress-actor ada --actor-id … --wordpress-id … --keypair ada.keypair.json`.
810
+ The command works before the plugin is enabled. Check the report: every
811
+ follower should be added, and any that were skipped should be re-run once
812
+ their servers answer.
813
+ 3. **Enable the WordPress plugin.** Enable it under `/admin/plugins`, then move
814
+ the DNS. Followers' servers go on delivering to
815
+ the plugin's old inbox paths until they next refetch the actor, and the
816
+ plugin is what catches those deliveries.
880
817
  4. **Watch.** `/admin/federation` lists the users with their actor ids and
881
- followers; `/admin/federation/settings` lists each compatibility path with
818
+ followers; the plugin's own screen under Plugins lists each old path with
882
819
  the instant it was last asked for. Deliveries should thin out as each
883
820
  follower's server refetches the actor and learns the new endpoints.
884
- 5. **Switch off.** Once every path says _Never_ again for long enough — weeks
885
- rather than days, since a quiet instance refetches rarely — clear the switch.
886
- The paths go away on the very next request, with nothing restarted, and if
887
- something was still using them you can turn it back on just as quickly.
821
+ 5. **Disable it.** Once every path says _Never_ again for long enough — weeks
822
+ rather than days, since a quiet instance refetches rarely — disable the
823
+ plugin. The paths go away on the very next request, with nothing restarted,
824
+ and if something was still using them you can enable it again just as
825
+ quickly.
888
826
 
889
827
  Retiring the stored actor id itself is a separate, optional step by the Move
890
828
  protocol, and is not part of the cutover: not every follower's software honours
@@ -1862,6 +1800,8 @@ shadow the login form.
1862
1800
  | `/admin/messages/read` | `POST` only. Marks one message read, or unread again. |
1863
1801
  | `/admin/messages/delete` | `POST` only. Deletes one message, and its file with it. |
1864
1802
  | `/admin/appearance/themes` | Appearance > Themes: the themes on disk. `POST` activates the one named. |
1803
+ | `/admin/plugins` | Plugins: every installed plugin, and Enable and Disable. |
1804
+ | `/admin/plugins/<package name>` | A running plugin's own screen, such as `/admin/plugins/@geekity/plugin-wordpress`. |
1865
1805
  | `/admin/tools` | Tools > Content index: what the index holds, and the button that rebuilds it. |
1866
1806
  | `/admin/tools/rebuild-index` | `POST` only. Offers the rebuild, then reads every file again on the live site. |
1867
1807
  | `/admin/settings` | Settings > General: title, tagline, author, base URL, time zone, language, locale, site icon. |
@@ -1882,7 +1822,7 @@ shadow the login form.
1882
1822
  | `/admin/users/notifications/mode` | `POST` only. Sets how often that notice arrives: as they arrive, hourly or daily. |
1883
1823
  | `/admin/users/delete` | `POST` only. Deletes the user the form names. |
1884
1824
  | `/admin/federation` | The actors, their followers, the inbox log, and per-post delivery. |
1885
- | `/admin/federation/settings` | Federation > Settings: the relays the site subscribes to, and the WordPress switch. |
1825
+ | `/admin/federation/settings` | Federation > Settings: the relays the site subscribes to. |
1886
1826
  | `/admin/federation/resend` | `POST` only. Sends one post to the followers again, as its file now reads. |
1887
1827
  | `/admin/setup` | First run: creates the first admin. Closed once a user exists. |
1888
1828
  | `/admin/login` | Username and password. |
@@ -2022,8 +1962,8 @@ pages under `/admin/settings` — General, Reading, Permalinks, Discussion and
2022
1962
  Email — each read that file, validate what was typed and write it back; nothing
2023
1963
  else remembers a setting, and `data/geekity.db` holds none of them. Federation's
2024
1964
  page is a sixth one of exactly the same kind, filed under its own section at
2025
- `/admin/federation/settings` rather than under Settings, because the relays and
2026
- the compatibility switch are about the section that holds the followers.
1965
+ `/admin/federation/settings` rather than under Settings, because the relays are
1966
+ about the section that holds the followers.
2027
1967
 
2028
1968
  Each page saves its own fields and no others, onto the file as re-read inside
2029
1969
  the write, so two people saving two different pages at the same moment both
@@ -2050,14 +1990,10 @@ and every other key it already had is kept, `feedSize` and anything a site put
2050
1990
  there included. A key it does not carry is the default, and a key of the wrong
2051
1991
  type is the default too: a hand-edited `site.json` cannot take the site down.
2052
1992
 
2053
- `homepage`, `postsPage` and `wordpressActivityPub` are the only three written
2054
- just when they have a value. The first two are WordPress's Reading choice, the
2055
- slug of the page served at `/` and the slug of the page whose own URL carries
2056
- the post listing, absent altogether on a site that shows its latest posts at
2057
- `/`; the third is the
2058
- [WordPress ActivityPub compatibility](#wordpress-activitypub-compatibility)
2059
- switch, absent until somebody turns it on and absent again when they turn it
2060
- off. A `postsPage` without a `homepage` is
1993
+ `homepage` and `postsPage` are the only two written just when they have a
1994
+ value: the slug of the page served at `/` and the slug of the page whose own
1995
+ URL carries the post listing, absent altogether on a site that shows its latest
1996
+ posts at `/`. A `postsPage` without a `homepage` is
2061
1997
  ignored, the listing being at `/` already, and a slug naming no published page
2062
1998
  is a site back on its latest posts. See [The front page](#the-front-page).
2063
1999
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  {% import "components/fields.njk" as field %}
10
10
  {% from "components/alert.njk" import alert %}
11
+ {% from "components/badge.njk" import badge %}
11
12
  {% from "components/button.njk" import button, buttonLink %}
12
13
  {% from "components/card.njk" import card, cardActions %}
13
14
  {% from "components/collapse.njk" import collapse %}
@@ -32,6 +33,44 @@
32
33
  {% endif %}
33
34
  {% endmacro %}
34
35
 
36
+ {% macro pluginActions(fieldId, buttons) %}
37
+ {% if buttons %}
38
+ <div class="flex flex-col gap-2" data-editor-actions="{{ fieldId }}">
39
+ {% for action in buttons %}
40
+ <div class="flex flex-col items-start gap-1" data-editor-action="{{ action.url }}">
41
+ {{ button(action.label, style='soft', hidden=true) }}
42
+ <p class="text-sm" role="status"></p>
43
+ <div class="flex w-full flex-col gap-2 rounded-box border border-base-300 p-3" role="group" aria-label="{{ action.label }}" hidden>
44
+ <p class="whitespace-pre-wrap break-words"></p>
45
+ <ul class="flex flex-col gap-1" data-editor-choices hidden></ul>
46
+ <template data-editor-choice>
47
+ <li>
48
+ <label class="flex items-start gap-2">
49
+ <input type="checkbox" class="checkbox checkbox-sm mt-0.5" form="editor-suggestions">
50
+ <span class="flex flex-col">
51
+ <span class="flex flex-wrap items-center gap-2">
52
+ <span class="break-all" data-choice-value></span>
53
+ {{ badge('', color='primary', style='soft') }}
54
+ </span>
55
+ <span class="text-sm text-base-content/70" data-choice-note></span>
56
+ </span>
57
+ </label>
58
+ </li>
59
+ </template>
60
+ <template data-editor-choice-group>
61
+ <li class="pt-1 text-sm font-semibold first:pt-0" data-choice-group></li>
62
+ </template>
63
+ <div class="flex gap-2">
64
+ {{ button('Accept', color='primary') }}
65
+ {{ button('Dismiss', style='ghost') }}
66
+ </div>
67
+ </div>
68
+ </div>
69
+ {% endfor %}
70
+ </div>
71
+ {% endif %}
72
+ {% endmacro %}
73
+
35
74
  {% block content %}
36
75
  <h1 class="mb-6 text-2xl font-bold">{{ heading }}</h1>
37
76
 
@@ -72,6 +111,7 @@
72
111
  {% set titleHint %}{% if kind.type == "post" %}Leave it empty to write a note: the post is then its words, and takes its slug from the first of them.{% endif %}{% endset %}
73
112
  {#- A post may go without one, and is then a note; a page is always named. -#}
74
113
  {{ field.text('editor-title', 'title', 'Title', value=form.title, required=(kind.type == "page"), autofocus=true, error=(error if errorField == 'editor-title' else ''), hint=titleHint) }}
114
+ {{ pluginActions('editor-title', editorActions['editor-title']) }}
75
115
 
76
116
  {{ field.textarea('editor-body', 'body', 'Body', value=form.body, rows=24) }}
77
117
 
@@ -242,6 +282,7 @@
242
282
  {% call collapse('Tags and categories', open.taxonomy, fields=true) %}
243
283
  {% if kind.tagged %}
244
284
  {{ field.text('editor-tags', 'tags', 'Tags', value=form.tags, hint='Separated by commas.') }}
285
+ {{ pluginActions('editor-tags', editorActions['editor-tags']) }}
245
286
  {% endif %}
246
287
  {% if kind.categorised %}
247
288
  {{ field.text('editor-categories', 'categories', 'Categories', value=form.categories, hint='Separated by commas.') }}
@@ -318,6 +359,7 @@
318
359
 
319
360
  {% call collapse('Summary and language', open.summary, fields=true) %}
320
361
  {{ field.text('editor-description', 'description', 'Description', value=form.description, error=(error if errorField == 'editor-description' else '')) }}
362
+ {{ pluginActions('editor-description', editorActions['editor-description']) }}
321
363
 
322
364
  {#- The `lang` front matter (TASK-154): a post or page written in another
323
365
  language than the site's says so, for screen readers, translation
@@ -367,6 +409,9 @@
367
409
  {% endcall %}
368
410
  </div>
369
411
  </form>
412
+ {% if editorActions | length %}
413
+ <form id="editor-suggestions" hidden></form>
414
+ {% endif %}
370
415
 
371
416
  {# CodeMirror, the preview tab and the upload control. A module script is
372
417
  deferred by definition, so it runs after the form is in the document.
@@ -392,4 +437,7 @@
392
437
  {% if kind.type == 'post' %}
393
438
  <script defer src="{{ assetPrefix }}location.js"></script>
394
439
  {% endif %}
440
+ {% if editorActions | length %}
441
+ <script defer src="{{ assetPrefix }}editor-actions.js"></script>
442
+ {% endif %}
395
443
  {% endblock %}
@@ -1,13 +1,10 @@
1
- {# /admin/federation/settings: which relays boost the site, and whether it is
2
- still answering the paths a WordPress site arrived with. It is Federation >
1
+ {# /admin/federation/settings: which relays boost the site. It is Federation >
3
2
  Settings rather than Settings > Federation (TASK-109): the section holds
4
3
  everything about federation, and Followers beside it is where the actors and
5
4
  the people following them are. Who the site's people are on the fediverse is
6
5
  a user's profile now (decision-14). #}
7
6
  {% extends "layouts/settings-page.njk" %}
8
7
  {% import "components/fields.njk" as field %}
9
- {% from "components/card.njk" import card %}
10
- {% from "components/table.njk" import table %}
11
8
 
12
9
  {% block settingsProblems %}
13
10
  {{ field.problem('settings-relays', problems.relays) }}
@@ -23,59 +20,4 @@
23
20
  {% endset %}
24
21
  {{ field.textarea('settings-relays', fields.relays, 'Relays', form.relays,
25
22
  error=problems.relays, hint=relaysHint, rows='3', spellcheck='false', autofocus=true) }}
26
-
27
- <h2 class="mt-6 text-lg font-semibold">WordPress</h2>
28
-
29
- {% set wordpressHint %}
30
- Answer the inbox and collection paths the WordPress ActivityPub plugin
31
- published, under <code>/wp-json/activitypub/1.0/</code>, for the users
32
- carrying its numeric actor id. Only a site that moved here from that plugin
33
- needs this: a follower's server keeps delivering to the inbox URL it cached
34
- until it next refetches the actor, and this is what catches those
35
- deliveries in the meantime.
36
- {% endset %}
37
- {{ field.checkbox('settings-wordpress-activitypub', fields.wordpressActivityPub,
38
- 'WordPress ActivityPub compatibility', checked=form.wordpressActivityPub,
39
- hint=wordpressHint) }}
40
- {% endblock %}
41
-
42
- {% block settingsPanels %}
43
- {# When each compatibility path was last asked for. It is the whole reason
44
- the instants are recorded: once every follower's server has refetched the
45
- actor, nothing asks any more and the switch can go off. #}
46
- {% call card(title='WordPress paths') %}
47
- {% if wordpressRoutes.length %}
48
- {% call table('WordPress paths', inCard=true) %}
49
- <thead>
50
- <tr><th scope="col">Path</th><th scope="col">For</th><th scope="col">Last asked for</th></tr>
51
- </thead>
52
- <tbody>
53
- {% for route in wordpressRoutes %}
54
- <tr>
55
- <td><code>{{ route.path }}</code></td>
56
- <td>{{ route.username or 'every user' }}</td>
57
- <td class="whitespace-nowrap">
58
- {% if route.lastRequestedAt %}
59
- {{ route.lastRequestedAt | date('readable') }}
60
- {% else %}
61
- Never
62
- {% endif %}
63
- </td>
64
- </tr>
65
- {% endfor %}
66
- </tbody>
67
- {% endcall %}
68
- <p class="text-sm">
69
- Once every one of these has been quiet for long enough that every
70
- follower's server has refetched the actor, turn the switch off: these
71
- paths are a cache other servers hold, not this site's identity, and the
72
- CMS carries them only until the caches have moved on.
73
- </p>
74
- {% else %}
75
- <p class="text-sm">
76
- No user carries a WordPress actor id, so the switch would serve nothing.
77
- The <code>geekity import wordpress-actor</code> command sets one.
78
- </p>
79
- {% endif %}
80
- {% endcall %}
81
23
  {% endblock %}
@@ -0,0 +1,113 @@
1
+ {# /admin/plugins: every installed plugin and where it stands (decision-33).
2
+
3
+ Enabling writes the plugins key of content/_data/site.json and takes effect
4
+ on the next request. A plugin's row names each plugin it requires with the
5
+ range it needs, linked to that plugin's row when it is installed, because
6
+ nothing is enabled on the operator's behalf. Under geekity serve, Reload
7
+ appears when the plugins folder differs from what this worker loaded
8
+ (TASK-288). #}
9
+ {% extends "layouts/shell.njk" %}
10
+ {% from "components/badge.njk" import status %}
11
+ {% from "components/button.njk" import button %}
12
+ {% from "components/card.njk" import card, cardActions %}
13
+
14
+ {% block title %}Plugins{% endblock %}
15
+
16
+ {% block content %}
17
+ <h1 class="mb-6 text-2xl font-bold">Plugins</h1>
18
+
19
+ {% if reloaded %}
20
+ <div role="status" class="alert alert-success mb-6"><span>Plugins reloaded.</span></div>
21
+ {% endif %}
22
+
23
+ {% if reloadFailure %}
24
+ <div role="alert" class="alert alert-error mb-6">
25
+ <span>The last reload failed, so this server carried on with the plugins it had. {{ reloadFailure }}</span>
26
+ </div>
27
+ {% endif %}
28
+
29
+ {% if reload %}
30
+ <div id="plugins-reload" class="mb-8 max-w-prose">
31
+ {% call card(title="The plugins folder has changed") %}
32
+ <p>Reload starts a new server with the plugins folder as it is now, then retires this one. No request is turned away.</p>
33
+ <dl class="text-sm">
34
+ {% for heading, names in [["Added", reload.changes.added], ["Removed", reload.changes.removed], ["Updated", reload.changes.updated]] %}
35
+ {% if names.length %}
36
+ <dt class="font-semibold">{{ heading }}:</dt>
37
+ <dd class="mb-2">
38
+ {% for name in names %}<code class="break-all">{{ name }}</code>{% if not loop.last %}, {% endif %}{% endfor %}
39
+ </dd>
40
+ {% endif %}
41
+ {% endfor %}
42
+ </dl>
43
+ <form method="post" action="{{ reload.url }}">
44
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
45
+ {% call cardActions() %}
46
+ {{ button('Reload', type='submit', color='primary') }}
47
+ {% endcall %}
48
+ </form>
49
+ {% endcall %}
50
+ </div>
51
+ {% endif %}
52
+
53
+ {% if not plugins.length %}
54
+ <p class="max-w-prose">
55
+ No plugins are installed. A site with its own <code>server.ts</code> passes them in
56
+ <code>plugins</code> on its config.
57
+ </p>
58
+ {% endif %}
59
+
60
+ <ul class="mb-8 grid grid-cols-1 gap-6 xl:grid-cols-2">
61
+ {% for plugin in plugins %}
62
+ <li id="{{ plugin.anchor }}">
63
+ {% call card(title=plugin.label) %}
64
+ <p class="flex flex-wrap items-center gap-2">
65
+ <code class="break-all">{{ plugin.name }}</code>
66
+ <span>{{ plugin.version }}</span>
67
+ {{ status(plugin.badge.status, plugin.badge.label) }}
68
+ </p>
69
+ <p>{{ plugin.description }}</p>
70
+ <p class="text-sm">From {{ plugin.source }}.</p>
71
+
72
+ {% if plugin.problem %}
73
+ <p class="text-sm text-error">{{ plugin.problem }}</p>
74
+ {% endif %}
75
+
76
+ {% if plugin.requirements.length %}
77
+ <div class="text-sm">
78
+ <p>Requires:</p>
79
+ <ul class="list-disc ps-5">
80
+ {% for requirement in plugin.requirements %}
81
+ <li>
82
+ {% if requirement.satisfied %}
83
+ <code>{{ requirement.name }}</code> <code>{{ requirement.range }}</code>, which is enabled.
84
+ {% else %}
85
+ Needs <code>{{ requirement.name }}</code> <code>{{ requirement.range }}</code>, which is {{ requirement.words }}.
86
+ {% if requirement.anchor %}
87
+ <a class="link" href="#{{ requirement.anchor }}">Go to it</a>.
88
+ {% endif %}
89
+ {% endif %}
90
+ </li>
91
+ {% endfor %}
92
+ </ul>
93
+ </div>
94
+ {% endif %}
95
+
96
+ {% if plugin.canEnable or plugin.canDisable %}
97
+ <form method="post" action="{{ enableUrl if plugin.canEnable else disableUrl }}">
98
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
99
+ <input type="hidden" name="{{ field }}" value="{{ plugin.name }}" />
100
+ {% call cardActions() %}
101
+ {% if plugin.canEnable %}
102
+ {{ button('Enable', type='submit', color='primary') }}
103
+ {% else %}
104
+ {{ button('Disable', type='submit') }}
105
+ {% endif %}
106
+ {% endcall %}
107
+ </form>
108
+ {% endif %}
109
+ {% endcall %}
110
+ </li>
111
+ {% endfor %}
112
+ </ul>
113
+ {% endblock %}