@micelio/strapi-plugin-core 0.1.0 → 0.2.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 (54) hide show
  1. package/README.md +5 -3
  2. package/dist/admin/MicelioLayout-Bmu5xCw9.js +1023 -0
  3. package/dist/admin/MicelioLayout-DMMsKnMo.mjs +1021 -0
  4. package/dist/admin/PreviewHeaderAction-BaduMvYy.mjs +61 -0
  5. package/dist/admin/PreviewHeaderAction-CGHycv4I.js +61 -0
  6. package/dist/admin/{en-D-T8z3qh.js → en-BDVuq0fN.js} +37 -1
  7. package/dist/admin/{en-C2O-S2ym.mjs → en-Bahc6y6N.mjs} +37 -1
  8. package/dist/admin/{es-RV0vkOHh.js → es-DStBGkm7.js} +37 -1
  9. package/dist/admin/{es-CQ2tv9N8.mjs → es-nUHBIoZG.mjs} +37 -1
  10. package/dist/admin/{index-D3TGbaQ7.js → index-D_Dbvo2F.js} +8 -2
  11. package/dist/admin/{index-CiSujTES.mjs → index-Dwr15ZsB.mjs} +8 -2
  12. package/dist/admin/index.js +1 -1
  13. package/dist/admin/index.mjs +1 -1
  14. package/dist/admin/permissions-BAI0qDr9.js +24 -0
  15. package/dist/admin/permissions-CYirM30e.mjs +25 -0
  16. package/dist/admin/src/components/EyeIcon.d.ts +8 -0
  17. package/dist/admin/src/components/PreviewFrame.d.ts +47 -0
  18. package/dist/admin/src/components/PreviewHeaderAction.d.ts +28 -0
  19. package/dist/admin/src/hooks/useEntry.d.ts +31 -0
  20. package/dist/admin/src/hooks/usePreviewSource.d.ts +31 -0
  21. package/dist/admin/src/pages/MicelioLayout.d.ts +2 -1
  22. package/dist/admin/src/pages/PreviewPage.d.ts +2 -0
  23. package/dist/admin/src/permissions.d.ts +5 -0
  24. package/dist/admin/src/translations/en.json.d.ts +37 -1
  25. package/dist/admin/src/utils/fetchError.d.ts +2 -0
  26. package/dist/admin/src/utils/previewAction.d.ts +22 -0
  27. package/dist/admin/src/utils/previewPane.d.ts +59 -0
  28. package/dist/server/index.js +796 -105
  29. package/dist/server/index.mjs +796 -105
  30. package/dist/server/src/constants/preview.d.ts +2 -0
  31. package/dist/server/src/constants/taxonomy.d.ts +13 -0
  32. package/dist/server/src/content-types/category/schema.json.d.ts +78 -0
  33. package/dist/server/src/content-types/index.d.ts +125 -2
  34. package/dist/server/src/content-types/preview-credential/schema.json.d.ts +7 -6
  35. package/dist/server/src/content-types/tag/schema.json.d.ts +46 -0
  36. package/dist/server/src/controllers/index.d.ts +6 -0
  37. package/dist/server/src/editorial/transitions.d.ts +5 -2
  38. package/dist/server/src/index.d.ts +140 -2
  39. package/dist/server/src/migrations/rename-legacy-uids.d.ts +26 -0
  40. package/dist/server/src/migrations/site-backfill.d.ts +13 -0
  41. package/dist/server/src/preview/credentials.d.ts +7 -5
  42. package/dist/server/src/preview/draft-query.d.ts +62 -0
  43. package/dist/server/src/preview/revocation.d.ts +9 -1
  44. package/dist/server/src/preview/shortcut.d.ts +24 -0
  45. package/dist/server/src/routes/index.d.ts +2 -0
  46. package/dist/server/src/routes/taxonomy.d.ts +12 -0
  47. package/dist/server/src/services/index.d.ts +6 -0
  48. package/dist/server/src/services/preview.d.ts +25 -6
  49. package/dist/server/src/site-scope/documents.d.ts +13 -3
  50. package/dist/server/src/types/hooks.d.ts +5 -4
  51. package/dist/server/src/types/preview.d.ts +35 -10
  52. package/package.json +4 -3
  53. package/dist/admin/MicelioLayout-3sKGPS_0.mjs +0 -443
  54. package/dist/admin/MicelioLayout-DXzRS4l3.js +0 -443
@@ -2,9 +2,9 @@ import { z } from "zod";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { factories } from "@strapi/strapi";
5
+ import { errors } from "@strapi/utils";
5
6
  import { createRequire } from "node:module";
6
7
  import path from "node:path";
7
- import { errors } from "@strapi/utils";
8
8
  import { Readable } from "node:stream";
9
9
  import { pipeline } from "node:stream/promises";
10
10
  import { createGzip, createGunzip } from "node:zlib";
@@ -12,7 +12,7 @@ import { createReadStream, createWriteStream, statSync } from "node:fs";
12
12
  import { mkdtemp, rm } from "node:fs/promises";
13
13
  import os from "node:os";
14
14
  import { once } from "node:events";
15
- const SITE_UID = "plugin::micelio.site";
15
+ const SITE_UID$1 = "plugin::micelio.site";
16
16
  const SITE_COMPONENT_CATEGORY = "micelio-site";
17
17
  const DEFAULT_SITE_SLUG = "default";
18
18
  const LOCALISED_LOCALES = ["en", "es"];
@@ -113,7 +113,7 @@ function parseExportSection(value) {
113
113
  function missingLocales(value) {
114
114
  return LOCALISED_LOCALES.filter((locale) => !value[locale]?.trim());
115
115
  }
116
- const attributeOf = (id2) => `${SITE_SECTION_ATTRIBUTE_PREFIX}${id2.replace(/-/g, "_")}`;
116
+ const attributeOf$1 = (id2) => `${SITE_SECTION_ATTRIBUTE_PREFIX}${id2.replace(/-/g, "_")}`;
117
117
  function createExtensionRegistry(strapi) {
118
118
  const pages = [];
119
119
  const sections = [];
@@ -150,8 +150,8 @@ function createExtensionRegistry(strapi) {
150
150
  "micelio: registerSiteSection: call it in `register`, before the models are built"
151
151
  );
152
152
  }
153
- const attribute = attributeOf(section.id);
154
- strapi.get("content-types").extend(SITE_UID, (contentType) => {
153
+ const attribute = attributeOf$1(section.id);
154
+ strapi.get("content-types").extend(SITE_UID$1, (contentType) => {
155
155
  Object.assign(contentType.attributes, {
156
156
  [attribute]: {
157
157
  type: "component",
@@ -180,7 +180,7 @@ function createExtensionRegistry(strapi) {
180
180
  siteSections: () => [...sections],
181
181
  statusChecks: () => [...checks],
182
182
  exportSections: () => [...exports],
183
- siteSectionAttribute: attributeOf,
183
+ siteSectionAttribute: attributeOf$1,
184
184
  async runStatusChecks(sites) {
185
185
  for (const registration of checks) {
186
186
  const found = [];
@@ -204,12 +204,12 @@ function createExtensionRegistry(strapi) {
204
204
  };
205
205
  }
206
206
  function hasDatabaseModels(strapi) {
207
- return strapi.db.metadata.has(SITE_UID);
207
+ return strapi.db.metadata.has(SITE_UID$1);
208
208
  }
209
209
  function extensionsOf(strapi) {
210
210
  return strapi.plugin("micelio").service("extensions");
211
211
  }
212
- const SITE$1 = SITE_UID;
212
+ const SITE$1 = SITE_UID$1;
213
213
  function siteSectionInstances(strapi) {
214
214
  return async (context, next) => {
215
215
  if (context.uid === SITE$1 && context.action === "create") {
@@ -342,7 +342,7 @@ function deepFreeze(value) {
342
342
  }
343
343
  return value;
344
344
  }
345
- const SITE_ATTRIBUTE = "site";
345
+ const SITE_ATTRIBUTE$1 = "site";
346
346
  const SITE_SCOPE_MIDDLEWARE = "plugin::micelio.site-scope";
347
347
  function withSiteScope(schema) {
348
348
  return {
@@ -353,10 +353,10 @@ function withSiteScope(schema) {
353
353
  },
354
354
  attributes: {
355
355
  ...schema.attributes,
356
- [SITE_ATTRIBUTE]: {
356
+ [SITE_ATTRIBUTE$1]: {
357
357
  type: "relation",
358
358
  relation: "manyToOne",
359
- target: SITE_UID,
359
+ target: SITE_UID$1,
360
360
  required: true,
361
361
  pluginOptions: { i18n: { localized: false } }
362
362
  }
@@ -381,7 +381,8 @@ const TRANSITIONS = [
381
381
  { from: "draft", to: "scheduled" },
382
382
  { from: "scheduled", to: "draft" },
383
383
  { from: "scheduled", to: "published" },
384
- { from: "published", to: "draft" }
384
+ { from: "published", to: "draft" },
385
+ { from: "published", to: "published" }
385
386
  ];
386
387
  const SYSTEM_TRANSITIONS = /* @__PURE__ */ new Set(["scheduled->published"]);
387
388
  const SUPER_ADMIN = "strapi-super-admin";
@@ -393,7 +394,8 @@ const DEFAULT_EDITORIAL_POLICY = {
393
394
  "in_review->published": [EDITOR, SUPER_ADMIN],
394
395
  "draft->scheduled": [EDITOR, SUPER_ADMIN],
395
396
  "scheduled->draft": [EDITOR, SUPER_ADMIN],
396
- "published->draft": [EDITOR, SUPER_ADMIN]
397
+ "published->draft": [EDITOR, SUPER_ADMIN],
398
+ "published->published": [EDITOR, SUPER_ADMIN]
397
399
  };
398
400
  function transitionKey({ from, to }) {
399
401
  return `${from}->${to}`;
@@ -449,7 +451,7 @@ async function performTransition(strapi, request) {
449
451
  status: "draft",
450
452
  populate: {
451
453
  [EDITORIAL_ATTRIBUTE]: { populate: ["reviewedBy", "publishedBy"] },
452
- [SITE_ATTRIBUTE]: { fields: ["documentId", "editorialPolicy"] },
454
+ [SITE_ATTRIBUTE$1]: { fields: ["documentId", "editorialPolicy"] },
453
455
  createdBy: true
454
456
  }
455
457
  });
@@ -508,7 +510,9 @@ async function performTransition(strapi, request) {
508
510
  throw new TransitionError(409, "editorialTransitionRefused", refusalMessage(error));
509
511
  }
510
512
  await runAsTransition(async () => {
511
- if (transition.from === "published") await documents.unpublish(target);
513
+ if (transition.from === "published" && transition.to !== "published") {
514
+ await documents.unpublish(target);
515
+ }
512
516
  await documents.update({ ...target, data: { [EDITORIAL_ATTRIBUTE]: editorial2 } });
513
517
  if (transition.to === "published") await documents.publish(target);
514
518
  });
@@ -1035,10 +1039,10 @@ async function storedFingerprint(strapi) {
1035
1039
  }
1036
1040
  const SITE_LOCALES = ["en", "es"];
1037
1041
  async function ensureDefaultSite(strapi) {
1038
- if (await strapi.db.query(SITE_UID).count() > 0) return false;
1042
+ if (await strapi.db.query(SITE_UID$1).count() > 0) return false;
1039
1043
  const locales = strapi.plugin("i18n").service("locales");
1040
1044
  const locale = await locales.getDefaultLocale();
1041
- await strapi.documents(SITE_UID).create({
1045
+ await strapi.documents(SITE_UID$1).create({
1042
1046
  locale,
1043
1047
  data: {
1044
1048
  name: "Micelio",
@@ -1052,6 +1056,28 @@ async function ensureDefaultSite(strapi) {
1052
1056
  });
1053
1057
  return true;
1054
1058
  }
1059
+ const SITE_UID = "plugin::micelio.site";
1060
+ const SITE_ATTRIBUTE = "site";
1061
+ const BACKFILLED = ["plugin::micelio.category", "plugin::micelio.tag"];
1062
+ async function backfillSite(strapi) {
1063
+ const site2 = await strapi.db.query(SITE_UID).findOne({
1064
+ where: { isDefault: true },
1065
+ select: ["id"]
1066
+ });
1067
+ if (!site2) return 0;
1068
+ let linked = 0;
1069
+ for (const uid of BACKFILLED) {
1070
+ const rows = await strapi.db.query(uid).findMany({
1071
+ where: { [SITE_ATTRIBUTE]: null },
1072
+ select: ["id"]
1073
+ });
1074
+ for (const row of rows) {
1075
+ await strapi.db.query(uid).update({ where: { id: row.id }, data: { [SITE_ATTRIBUTE]: site2.id } });
1076
+ }
1077
+ linked += rows.length;
1078
+ }
1079
+ return linked;
1080
+ }
1055
1081
  async function registerPreviewPermission(strapi) {
1056
1082
  await strapi.service("admin::permission").actionProvider.registerMany([
1057
1083
  {
@@ -1062,9 +1088,13 @@ async function registerPreviewPermission(strapi) {
1062
1088
  }
1063
1089
  ]);
1064
1090
  }
1091
+ const ADMIN_LOGOUT_HANDLER = "authentication.logout";
1065
1092
  function registerPreviewRevocation(strapi) {
1066
- strapi.eventHub.on("admin.logout", async (event) => {
1067
- const id2 = event.user?.id;
1093
+ strapi.server.use(async (ctx, next) => {
1094
+ await next();
1095
+ const route = ctx.state.route;
1096
+ if (route?.info?.pluginName !== "admin" || route.handler !== ADMIN_LOGOUT_HANDLER) return;
1097
+ const id2 = ctx.state.user?.id;
1068
1098
  if (typeof id2 !== "number") return;
1069
1099
  try {
1070
1100
  await strapi.plugin("micelio").service("preview").revoke(id2);
@@ -1087,6 +1117,9 @@ async function registerStatusPermission(strapi) {
1087
1117
  }
1088
1118
  const bootstrap = async ({ strapi }) => {
1089
1119
  await ensureDefaultSite(strapi);
1120
+ const backfilled = await backfillSite(strapi);
1121
+ if (backfilled > 0)
1122
+ strapi.log.info(`[micelio] gave the default site to ${backfilled} categories and tags`);
1090
1123
  await ensureSiteSections(strapi);
1091
1124
  await strapi.plugin("micelio").service("hostCheck").enforce();
1092
1125
  registerScheduler(strapi);
@@ -1100,7 +1133,7 @@ const DEFAULT_CONFIG = {
1100
1133
  scheduledPublishRule: "* * * * *",
1101
1134
  statusRefreshRule: "0 4 * * *"
1102
1135
  };
1103
- const config = {
1136
+ const config$1 = {
1104
1137
  default: DEFAULT_CONFIG,
1105
1138
  validator(config2) {
1106
1139
  for (const key of ["scheduledPublishRule", "statusRefreshRule"]) {
@@ -1111,13 +1144,41 @@ const config = {
1111
1144
  }
1112
1145
  }
1113
1146
  };
1147
+ const kind$3 = "collectionType";
1148
+ const collectionName$a = "categories";
1149
+ const info$a = { "singularName": "category", "pluralName": "categories", "displayName": "Category", "description": "Organize your content into categories" };
1150
+ const options$a = { "draftAndPublish": false };
1151
+ const pluginOptions$3 = { "content-manager": { "visible": true }, "i18n": { "localized": true } };
1152
+ const attributes$a = { "name": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } }, "slug": { "type": "uid", "regex": "^(?!\\.{1,2}$)[A-Za-z0-9-_.~]*$", "pluginOptions": { "i18n": { "localized": false } } }, "description": { "type": "text", "pluginOptions": { "i18n": { "localized": true } } }, "order": { "type": "integer", "pluginOptions": { "i18n": { "localized": false } } }, "featured": { "type": "boolean", "default": false, "pluginOptions": { "i18n": { "localized": false } } }, "color": { "type": "integer", "min": 1, "max": 5, "pluginOptions": { "i18n": { "localized": false } } } };
1153
+ const category = {
1154
+ kind: kind$3,
1155
+ collectionName: collectionName$a,
1156
+ info: info$a,
1157
+ options: options$a,
1158
+ pluginOptions: pluginOptions$3,
1159
+ attributes: attributes$a
1160
+ };
1161
+ const kind$2 = "collectionType";
1162
+ const collectionName$9 = "micelio_preview_credentials";
1163
+ const info$9 = { "singularName": "preview-credential", "pluralName": "preview-credentials", "displayName": "Preview credential", "description": "A preview token, grant or system grant of ADR-0019, stored as the hash of its value and bound to one draft (and, but for a system grant, to one admin user)" };
1164
+ const options$9 = { "draftAndPublish": false };
1165
+ const pluginOptions$2 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false }, "i18n": { "localized": false }, "micelio": { "export": "never" } };
1166
+ const attributes$9 = { "kind": { "type": "enumeration", "enum": ["token", "grant", "system"], "required": true }, "hash": { "type": "string", "required": true, "unique": true, "private": true }, "adminUser": { "type": "integer" }, "adminOrigin": { "type": "string" }, "entryVersion": { "type": "string" }, "siteDocumentId": { "type": "string", "required": true }, "contentType": { "type": "string", "required": true }, "entryDocumentId": { "type": "string", "required": true }, "entryLocale": { "type": "string" }, "expiresAt": { "type": "datetime", "required": true }, "usedAt": { "type": "datetime" }, "revokedAt": { "type": "datetime" } };
1167
+ const previewCredential = {
1168
+ kind: kind$2,
1169
+ collectionName: collectionName$9,
1170
+ info: info$9,
1171
+ options: options$9,
1172
+ pluginOptions: pluginOptions$2,
1173
+ attributes: attributes$9
1174
+ };
1114
1175
  const kind$1 = "collectionType";
1115
- const collectionName$8 = "micelio_preview_credentials";
1116
- const info$8 = { "singularName": "preview-credential", "pluralName": "preview-credentials", "displayName": "Preview credential", "description": "A preview token or grant of ADR-0019, stored as the hash of its value and bound to one admin user and one draft" };
1176
+ const collectionName$8 = "micelio_sites";
1177
+ const info$8 = { "singularName": "site", "pluralName": "sites", "displayName": "Site", "description": "One site the CMS serves: its identity, its host names and the modules the frontend turns on" };
1117
1178
  const options$8 = { "draftAndPublish": false };
1118
- const pluginOptions$1 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false }, "i18n": { "localized": false }, "micelio": { "export": "never" } };
1119
- const attributes$8 = { "kind": { "type": "enumeration", "enum": ["token", "grant"], "required": true }, "hash": { "type": "string", "required": true, "unique": true, "private": true }, "adminUser": { "type": "integer", "required": true }, "adminOrigin": { "type": "string", "required": true }, "siteDocumentId": { "type": "string", "required": true }, "contentType": { "type": "string", "required": true }, "entryDocumentId": { "type": "string", "required": true }, "entryLocale": { "type": "string" }, "expiresAt": { "type": "datetime", "required": true }, "usedAt": { "type": "datetime" }, "revokedAt": { "type": "datetime" } };
1120
- const previewCredential = {
1179
+ const pluginOptions$1 = { "content-manager": { "visible": true }, "content-type-builder": { "visible": false }, "i18n": { "localized": true } };
1180
+ const attributes$8 = { "name": { "type": "string", "required": true, "pluginOptions": { "i18n": { "localized": true } } }, "slug": { "type": "string", "regex": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 64, "required": true, "unique": true, "pluginOptions": { "i18n": { "localized": false } } }, "hosts": { "type": "json", "pluginOptions": { "i18n": { "localized": false } } }, "isDefault": { "type": "boolean", "default": false, "required": true, "pluginOptions": { "i18n": { "localized": false } } }, "editorialPolicy": { "type": "json", "pluginOptions": { "i18n": { "localized": false } } }, "description": { "type": "text", "pluginOptions": { "i18n": { "localized": true } } }, "tagline": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } }, "url": { "type": "string", "regex": "^https?://\\S+$", "pluginOptions": { "i18n": { "localized": false } } }, "defaultLocale": { "type": "enumeration", "enum": ["en", "es"], "default": "en", "required": true, "pluginOptions": { "i18n": { "localized": false } } }, "author": { "type": "component", "repeatable": false, "component": "micelio-site.author", "pluginOptions": { "i18n": { "localized": false } } }, "logo": { "type": "media", "multiple": false, "allowedTypes": ["images"], "pluginOptions": { "i18n": { "localized": false } } }, "favicon": { "type": "media", "multiple": false, "allowedTypes": ["images"], "pluginOptions": { "i18n": { "localized": false } } }, "defaultOgImage": { "type": "media", "multiple": false, "allowedTypes": ["images"], "pluginOptions": { "i18n": { "localized": false } } }, "socialLinks": { "type": "component", "repeatable": true, "component": "micelio-site.social-link", "pluginOptions": { "i18n": { "localized": false } } }, "contactEmail": { "type": "email", "pluginOptions": { "i18n": { "localized": false } } }, "privacyContactEmail": { "type": "email", "pluginOptions": { "i18n": { "localized": false } } }, "securityContact": { "type": "string", "maxLength": 512, "pluginOptions": { "i18n": { "localized": false } } }, "privacyJurisdiction": { "type": "enumeration", "enum": ["colombia"], "pluginOptions": { "i18n": { "localized": false } } }, "privacyUpdatedAt": { "type": "datetime", "pluginOptions": { "i18n": { "localized": false } } }, "supportHandle": { "type": "string", "regex": "^[A-Za-z0-9_-]+$", "pluginOptions": { "i18n": { "localized": false } } }, "timezone": { "type": "string", "regex": "^[A-Za-z][A-Za-z0-9]*(?:[/_+-][A-Za-z0-9]+)*$", "pluginOptions": { "i18n": { "localized": false } } }, "addressLocality": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } }, "modules": { "type": "component", "repeatable": false, "component": "micelio-site.modules", "required": true, "pluginOptions": { "i18n": { "localized": false } } }, "theme": { "type": "component", "repeatable": false, "component": "micelio-site.theme", "pluginOptions": { "i18n": { "localized": false } } }, "fediverse": { "type": "component", "repeatable": false, "component": "micelio-site.fediverse", "pluginOptions": { "i18n": { "localized": false } } } };
1181
+ const site = {
1121
1182
  kind: kind$1,
1122
1183
  collectionName: collectionName$8,
1123
1184
  info: info$8,
@@ -1126,12 +1187,12 @@ const previewCredential = {
1126
1187
  attributes: attributes$8
1127
1188
  };
1128
1189
  const kind = "collectionType";
1129
- const collectionName$7 = "micelio_sites";
1130
- const info$7 = { "singularName": "site", "pluralName": "sites", "displayName": "Site", "description": "One site the CMS serves: its identity, its host names and the modules the frontend turns on" };
1190
+ const collectionName$7 = "tags";
1191
+ const info$7 = { "singularName": "tag", "pluralName": "tags", "displayName": "Tag", "description": "Topics an article covers, used to filter the blog" };
1131
1192
  const options$7 = { "draftAndPublish": false };
1132
- const pluginOptions = { "content-manager": { "visible": true }, "content-type-builder": { "visible": false }, "i18n": { "localized": true } };
1133
- const attributes$7 = { "name": { "type": "string", "required": true, "pluginOptions": { "i18n": { "localized": true } } }, "slug": { "type": "string", "regex": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 64, "required": true, "unique": true, "pluginOptions": { "i18n": { "localized": false } } }, "hosts": { "type": "json", "pluginOptions": { "i18n": { "localized": false } } }, "isDefault": { "type": "boolean", "default": false, "required": true, "pluginOptions": { "i18n": { "localized": false } } }, "editorialPolicy": { "type": "json", "pluginOptions": { "i18n": { "localized": false } } }, "description": { "type": "text", "pluginOptions": { "i18n": { "localized": true } } }, "tagline": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } }, "url": { "type": "string", "regex": "^https?://\\S+$", "pluginOptions": { "i18n": { "localized": false } } }, "defaultLocale": { "type": "enumeration", "enum": ["en", "es"], "default": "en", "required": true, "pluginOptions": { "i18n": { "localized": false } } }, "author": { "type": "component", "repeatable": false, "component": "micelio-site.author", "pluginOptions": { "i18n": { "localized": false } } }, "logo": { "type": "media", "multiple": false, "allowedTypes": ["images"], "pluginOptions": { "i18n": { "localized": false } } }, "favicon": { "type": "media", "multiple": false, "allowedTypes": ["images"], "pluginOptions": { "i18n": { "localized": false } } }, "defaultOgImage": { "type": "media", "multiple": false, "allowedTypes": ["images"], "pluginOptions": { "i18n": { "localized": false } } }, "socialLinks": { "type": "component", "repeatable": true, "component": "micelio-site.social-link", "pluginOptions": { "i18n": { "localized": false } } }, "contactEmail": { "type": "email", "pluginOptions": { "i18n": { "localized": false } } }, "privacyContactEmail": { "type": "email", "pluginOptions": { "i18n": { "localized": false } } }, "securityContact": { "type": "string", "maxLength": 512, "pluginOptions": { "i18n": { "localized": false } } }, "privacyJurisdiction": { "type": "enumeration", "enum": ["colombia"], "pluginOptions": { "i18n": { "localized": false } } }, "privacyUpdatedAt": { "type": "datetime", "pluginOptions": { "i18n": { "localized": false } } }, "supportHandle": { "type": "string", "regex": "^[A-Za-z0-9_-]+$", "pluginOptions": { "i18n": { "localized": false } } }, "timezone": { "type": "string", "regex": "^[A-Za-z][A-Za-z0-9]*(?:[/_+-][A-Za-z0-9]+)*$", "pluginOptions": { "i18n": { "localized": false } } }, "addressLocality": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } }, "modules": { "type": "component", "repeatable": false, "component": "micelio-site.modules", "required": true, "pluginOptions": { "i18n": { "localized": false } } }, "theme": { "type": "component", "repeatable": false, "component": "micelio-site.theme", "pluginOptions": { "i18n": { "localized": false } } }, "fediverse": { "type": "component", "repeatable": false, "component": "micelio-site.fediverse", "pluginOptions": { "i18n": { "localized": false } } } };
1134
- const site = {
1193
+ const pluginOptions = { "content-manager": { "visible": true }, "i18n": { "localized": true } };
1194
+ const attributes$7 = { "name": { "type": "string", "required": true, "pluginOptions": { "i18n": { "localized": true } } }, "slug": { "type": "uid", "regex": "^(?!\\.{1,2}$)[A-Za-z0-9-_.~]*$", "targetField": "name", "required": true, "pluginOptions": { "i18n": { "localized": false } } } };
1195
+ const tag = {
1135
1196
  kind,
1136
1197
  collectionName: collectionName$7,
1137
1198
  info: info$7,
@@ -1141,8 +1202,13 @@ const site = {
1141
1202
  };
1142
1203
  const contentTypes = {
1143
1204
  site: { schema: site },
1205
+ category: { schema: withSiteScope(category) },
1206
+ tag: { schema: withSiteScope(tag) },
1144
1207
  "preview-credential": { schema: withSiteScope(previewCredential) }
1145
1208
  };
1209
+ const CATEGORY_UID = "plugin::micelio.category";
1210
+ const TAG_UID = "plugin::micelio.tag";
1211
+ const READ_ACTIONS$1 = [`${CATEGORY_UID}.find`, `${TAG_UID}.find`];
1146
1212
  const blocks = () => ({
1147
1213
  find(ctx) {
1148
1214
  ctx.body = {
@@ -1160,18 +1226,19 @@ const PREVIEW_PATH = "/_preview";
1160
1226
  const PREVIEW_GRANT_HEADER = "x-micelio-preview-grant";
1161
1227
  const PREVIEW_TOKEN_TTL_SECONDS = 60;
1162
1228
  const PREVIEW_GRANT_TTL_SECONDS = 15 * 60;
1229
+ const PREVIEW_SYSTEM_GRANT_TTL_SECONDS = 60;
1163
1230
  const PREVIEW_CREDENTIAL_RETENTION_SECONDS = 60 * 60;
1164
1231
  const PREVIEW_RATE_WINDOW_SECONDS = 60;
1165
1232
  const PREVIEW_TOKEN_RATE_LIMIT = 20;
1166
1233
  const PREVIEW_EXCHANGE_RATE_LIMIT = 60;
1167
1234
  async function readableDraft(strapi, userAbility, contentType, documentId, locale) {
1168
1235
  const uid = contentType;
1169
- if (!strapi.contentType(uid)?.attributes?.[SITE_ATTRIBUTE]) return { status: "not-site-scoped" };
1236
+ if (!strapi.contentType(uid)?.attributes?.[SITE_ATTRIBUTE$1]) return { status: "not-site-scoped" };
1170
1237
  const entry = await strapi.documents(uid).findOne({
1171
1238
  documentId,
1172
1239
  locale: locale ?? void 0,
1173
1240
  status: "draft",
1174
- populate: { [SITE_ATTRIBUTE]: { fields: ["documentId"] }, createdBy: true }
1241
+ populate: { [SITE_ATTRIBUTE$1]: { fields: ["documentId"] }, createdBy: true }
1175
1242
  });
1176
1243
  if (!entry?.site) return { status: "missing" };
1177
1244
  const checker = strapi.plugin("content-manager").service("permission-checker").create({ userAbility, model: contentType });
@@ -1186,9 +1253,338 @@ async function abilityOf(strapi, adminUser) {
1186
1253
  if (!user || user.isActive === false) return null;
1187
1254
  return strapi.service("admin::permission").engine.generateUserAbility(user);
1188
1255
  }
1256
+ const FILE_UID = "plugin::upload.file";
1257
+ const QUERY_KEYS = /* @__PURE__ */ new Set(["populate", "site"]);
1258
+ const ALWAYS_FIELDS = /* @__PURE__ */ new Set(["id", "documentId", "createdAt", "updatedAt"]);
1259
+ const NOT_SCALAR = /* @__PURE__ */ new Set(["relation", "component", "dynamiczone", "media"]);
1260
+ const NO_POPULATE = { all: false, children: /* @__PURE__ */ new Map() };
1261
+ class QueryProblem extends Error {
1262
+ }
1263
+ function readDraftQuery(strapi, query, schema) {
1264
+ try {
1265
+ for (const key of Object.keys(query)) {
1266
+ if (!QUERY_KEYS.has(key)) {
1267
+ throw new QueryProblem(
1268
+ `The draft read takes only documentId, locale and populate, not "${key}"`
1269
+ );
1270
+ }
1271
+ }
1272
+ return { populate: parsePopulate(strapi, query.populate, schema, "document", "populate") };
1273
+ } catch (error) {
1274
+ if (error instanceof QueryProblem) return { problem: error.message };
1275
+ throw error;
1276
+ }
1277
+ }
1278
+ async function readDraft(strapi, options2) {
1279
+ const schema = strapi.contentType(options2.uid);
1280
+ const params = await strapi.contentAPI.sanitize.query(
1281
+ { populate: draftPopulate(strapi, options2.populate, schema) },
1282
+ schema,
1283
+ { auth: options2.auth }
1284
+ );
1285
+ const entry = await strapi.documents(options2.uid).findOne({
1286
+ ...params,
1287
+ documentId: options2.documentId,
1288
+ locale: options2.locale,
1289
+ status: "draft"
1290
+ });
1291
+ if (!entry) return null;
1292
+ await publishedRelations(strapi, entry, schema, options2.populate);
1293
+ await keepToSite(strapi, entry, schema, options2.siteDocumentId);
1294
+ return entry;
1295
+ }
1296
+ function fail(message) {
1297
+ throw new QueryProblem(message);
1298
+ }
1299
+ function attributeOf(schema, name2) {
1300
+ return Object.prototype.hasOwnProperty.call(schema.attributes, name2) ? schema.attributes[name2] : void 0;
1301
+ }
1302
+ function expand(value, path2) {
1303
+ const found = { all: false, pairs: [] };
1304
+ const add2 = (part2) => {
1305
+ if (part2 === void 0 || part2 === null || part2 === false || part2 === "false") return;
1306
+ if (part2 === true || part2 === "true") {
1307
+ found.all = true;
1308
+ } else if (typeof part2 === "string") {
1309
+ for (const name2 of part2.split(",").map((piece) => piece.trim())) {
1310
+ if (name2 === "") continue;
1311
+ if (name2 === "*") found.all = true;
1312
+ else {
1313
+ const [head = "", ...rest] = name2.split(".");
1314
+ found.pairs.push([head, rest.length > 0 ? { populate: rest.join(".") } : true]);
1315
+ }
1316
+ }
1317
+ } else if (Array.isArray(part2)) {
1318
+ for (const item of part2) {
1319
+ if (typeof item !== "string" && (typeof item !== "object" || item === null)) {
1320
+ fail(`${path2} must be a name, a list of names or an object`);
1321
+ }
1322
+ add2(item);
1323
+ }
1324
+ } else if (typeof part2 === "object") {
1325
+ found.pairs.push(...Object.entries(part2));
1326
+ } else {
1327
+ fail(`${path2} must be a name, a list of names or an object`);
1328
+ }
1329
+ };
1330
+ add2(value);
1331
+ return found;
1332
+ }
1333
+ function parsePopulate(strapi, value, schema, scope, path2) {
1334
+ const { all: all2, pairs } = expand(value, path2);
1335
+ if (all2 && pairs.length > 0) fail(`${path2} cannot mix "*" with names`);
1336
+ if (all2 && scope === "related") fail(`${path2} cannot be "*" under a relation`);
1337
+ const children = /* @__PURE__ */ new Map();
1338
+ for (const [name2, sub] of pairs) {
1339
+ const here = `${path2}[${name2}]`;
1340
+ const attribute = attributeOf(schema, name2);
1341
+ if (!attribute || attribute.private === true) fail(`${here} is not an attribute of the type`);
1342
+ if (scope === "related" && attribute.type !== "media") {
1343
+ fail(`${here}: under a relation only a media attribute may be populated`);
1344
+ }
1345
+ if (children.has(name2)) fail(`${here} is named twice`);
1346
+ const node = parseNode(strapi, sub, attribute, here);
1347
+ if (node) children.set(name2, node);
1348
+ }
1349
+ return { all: all2, children };
1350
+ }
1351
+ function parseNode(strapi, sub, attribute, path2) {
1352
+ if (sub === false || sub === "false") return void 0;
1353
+ const options2 = sub === true || sub === "true" || sub === "" ? {} : sub;
1354
+ if (typeof options2 !== "object" || options2 === null || Array.isArray(options2)) {
1355
+ fail(`${path2} must be true or an object`);
1356
+ }
1357
+ const given = options2;
1358
+ const only = (...keys) => {
1359
+ for (const key of Object.keys(given)) {
1360
+ if (!keys.includes(key)) fail(`${path2} does not take "${key}"`);
1361
+ }
1362
+ };
1363
+ switch (attribute.type) {
1364
+ case "media":
1365
+ only("fields");
1366
+ return { fields: fieldsOf(given.fields, strapi.contentType(FILE_UID), path2) };
1367
+ case "relation": {
1368
+ only("fields", "populate");
1369
+ const target = relationTarget(strapi, attribute, path2);
1370
+ const schema = strapi.contentType(target);
1371
+ const node = { fields: fieldsOf(given.fields, schema, path2) };
1372
+ if (given.populate !== void 0) {
1373
+ node.populate = parsePopulate(
1374
+ strapi,
1375
+ given.populate,
1376
+ schema,
1377
+ "related",
1378
+ `${path2}[populate]`
1379
+ );
1380
+ }
1381
+ return node;
1382
+ }
1383
+ case "component":
1384
+ only("fields", "populate");
1385
+ return componentNode(strapi, given, attribute.component, path2);
1386
+ case "dynamiczone": {
1387
+ only("on");
1388
+ if (given.on === void 0) return {};
1389
+ if (typeof given.on !== "object" || given.on === null || Array.isArray(given.on)) {
1390
+ fail(`${path2}[on] must be an object`);
1391
+ }
1392
+ const on = /* @__PURE__ */ new Map();
1393
+ for (const [uid, fragment] of Object.entries(given.on)) {
1394
+ if (!attribute.components?.includes(uid))
1395
+ fail(`${path2}[on][${uid}] is not a component of the zone`);
1396
+ const node = parseNode(
1397
+ strapi,
1398
+ fragment,
1399
+ { type: "component", component: uid },
1400
+ `${path2}[on][${uid}]`
1401
+ );
1402
+ if (node) on.set(uid, node);
1403
+ }
1404
+ return { on };
1405
+ }
1406
+ default:
1407
+ return fail(`${path2} is not a relation, a component, a dynamic zone or a media attribute`);
1408
+ }
1409
+ }
1410
+ function componentNode(strapi, given, uid, path2) {
1411
+ const component2 = strapi.components[uid];
1412
+ const node = { fields: fieldsOf(given.fields, component2, path2) };
1413
+ if (given.populate !== void 0) {
1414
+ node.populate = parsePopulate(
1415
+ strapi,
1416
+ given.populate,
1417
+ component2,
1418
+ "document",
1419
+ `${path2}[populate]`
1420
+ );
1421
+ }
1422
+ return node;
1423
+ }
1424
+ function relationTarget(strapi, attribute, path2) {
1425
+ const target = attribute.target;
1426
+ if (typeof target !== "string" || target.startsWith("admin::") || !(target in strapi.contentTypes)) {
1427
+ return fail(`${path2} is a relation the draft read does not follow`);
1428
+ }
1429
+ return target;
1430
+ }
1431
+ function fieldsOf(value, schema, path2) {
1432
+ if (value === void 0) return void 0;
1433
+ const names = (Array.isArray(value) ? value : [value]).flatMap((item) => {
1434
+ if (typeof item !== "string") return fail(`${path2}[fields] must be names of fields`);
1435
+ return item.split(",").map((piece) => piece.trim());
1436
+ });
1437
+ for (const name2 of names) {
1438
+ const attribute = attributeOf(schema, name2);
1439
+ const known = ALWAYS_FIELDS.has(name2) || attribute !== void 0 && attribute.private !== true;
1440
+ if (!known || attribute !== void 0 && NOT_SCALAR.has(attribute.type)) {
1441
+ fail(`${path2}[fields]: "${name2}" is not a field of the type`);
1442
+ }
1443
+ }
1444
+ return names.filter((name2) => name2 !== "");
1445
+ }
1446
+ function draftPopulate(strapi, spec, schema) {
1447
+ if (spec.all) return "*";
1448
+ const populate = {};
1449
+ for (const [name2, node] of spec.children) {
1450
+ const attribute = attributeOf(schema, name2);
1451
+ if (attribute?.type === "media") {
1452
+ populate[name2] = node.fields ? { fields: node.fields } : true;
1453
+ } else if (attribute?.type === "component") {
1454
+ populate[name2] = componentPopulate(strapi, node, attribute.component);
1455
+ } else if (attribute?.type === "dynamiczone") {
1456
+ populate[name2] = node.on ? {
1457
+ on: Object.fromEntries(
1458
+ [...node.on].map(([uid, fragment]) => [
1459
+ uid,
1460
+ componentPopulate(strapi, fragment, uid)
1461
+ ])
1462
+ )
1463
+ } : true;
1464
+ } else {
1465
+ populate[name2] = true;
1466
+ }
1467
+ }
1468
+ return populate;
1469
+ }
1470
+ function componentPopulate(strapi, node, uid) {
1471
+ const options2 = {};
1472
+ if (node.fields) options2.fields = node.fields;
1473
+ if (node.populate) options2.populate = draftPopulate(strapi, node.populate, strapi.components[uid]);
1474
+ return Object.keys(options2).length > 0 ? options2 : true;
1475
+ }
1476
+ async function publishedRelations(strapi, entry, schema, spec) {
1477
+ for (const [name2, attribute] of Object.entries(schema.attributes)) {
1478
+ const value = entry[name2];
1479
+ if (value === void 0 || value === null) continue;
1480
+ const node = spec.children.get(name2) ?? (spec.all ? {} : void 0);
1481
+ if (attribute.type === "relation") {
1482
+ if (typeof attribute.target !== "string") {
1483
+ delete entry[name2];
1484
+ continue;
1485
+ }
1486
+ const target = attribute.target;
1487
+ const published = [];
1488
+ for (const related of Array.isArray(value) ? value : [value]) {
1489
+ const read = await publishedOf(strapi, target, related, node ?? {});
1490
+ if (read) published.push(read);
1491
+ }
1492
+ entry[name2] = Array.isArray(value) ? published : published[0] ?? null;
1493
+ } else if (attribute.type === "component") {
1494
+ const component2 = strapi.components[attribute.component];
1495
+ for (const child of Array.isArray(value) ? value : [value]) {
1496
+ await publishedRelations(strapi, child, component2, node?.populate ?? NO_POPULATE);
1497
+ }
1498
+ } else if (attribute.type === "dynamiczone") {
1499
+ for (const child of value) {
1500
+ const uid = child.__component;
1501
+ if (!(uid in strapi.components)) continue;
1502
+ await publishedRelations(
1503
+ strapi,
1504
+ child,
1505
+ strapi.components[uid],
1506
+ node?.on?.get(uid)?.populate ?? NO_POPULATE
1507
+ );
1508
+ }
1509
+ }
1510
+ }
1511
+ }
1512
+ async function publishedOf(strapi, target, related, node) {
1513
+ if (typeof related.documentId !== "string") return null;
1514
+ const media2 = {};
1515
+ for (const [name2, child] of node.populate?.children ?? []) {
1516
+ media2[name2] = child.fields ? { fields: child.fields } : true;
1517
+ }
1518
+ return await strapi.documents(target).findOne({
1519
+ documentId: related.documentId,
1520
+ locale: typeof related.locale === "string" ? related.locale : void 0,
1521
+ status: "published",
1522
+ fields: node.fields,
1523
+ populate: Object.keys(media2).length > 0 ? media2 : void 0
1524
+ });
1525
+ }
1526
+ async function keepToSite(strapi, entry, schema, siteDocumentId) {
1527
+ const found = /* @__PURE__ */ new Map();
1528
+ await walk(strapi, entry, schema, async (target, entries) => {
1529
+ if (!isSiteScoped(strapi.contentType(target))) return entries;
1530
+ const ids = found.get(target) ?? /* @__PURE__ */ new Set();
1531
+ for (const { documentId } of entries) if (typeof documentId === "string") ids.add(documentId);
1532
+ found.set(target, ids);
1533
+ return entries;
1534
+ });
1535
+ if (found.size === 0) return;
1536
+ const owned = /* @__PURE__ */ new Map();
1537
+ for (const [target, ids] of found) {
1538
+ const published = strapi.contentType(target).options?.draftAndPublish === true;
1539
+ const rows = await strapi.db.query(target).findMany({
1540
+ select: ["documentId"],
1541
+ where: {
1542
+ documentId: { $in: [...ids] },
1543
+ [SITE_ATTRIBUTE$1]: { documentId: siteDocumentId },
1544
+ ...published ? { publishedAt: { $notNull: true } } : {}
1545
+ }
1546
+ });
1547
+ owned.set(target, new Set(rows.map((row) => row.documentId)));
1548
+ }
1549
+ await walk(strapi, entry, schema, async (target, entries) => {
1550
+ const ids = owned.get(target);
1551
+ if (!isSiteScoped(strapi.contentType(target))) return entries;
1552
+ return entries.filter(
1553
+ ({ documentId }) => typeof documentId === "string" && ids?.has(documentId) === true
1554
+ );
1555
+ });
1556
+ }
1557
+ async function walk(strapi, entry, schema, visit) {
1558
+ for (const [name2, attribute] of Object.entries(schema.attributes)) {
1559
+ const value = entry[name2];
1560
+ if (value === void 0 || value === null) continue;
1561
+ if (attribute.type === "relation") {
1562
+ if (!("target" in attribute) || typeof attribute.target !== "string") {
1563
+ delete entry[name2];
1564
+ continue;
1565
+ }
1566
+ const target = attribute.target;
1567
+ const many = Array.isArray(value);
1568
+ const kept = await visit(target, many ? value : [value]);
1569
+ for (const child of kept) await walk(strapi, child, strapi.contentType(target), visit);
1570
+ entry[name2] = many ? kept : kept[0] ?? null;
1571
+ } else if (attribute.type === "component") {
1572
+ const component2 = strapi.components[attribute.component];
1573
+ for (const child of Array.isArray(value) ? value : [value]) {
1574
+ await walk(strapi, child, component2, visit);
1575
+ }
1576
+ } else if (attribute.type === "dynamiczone") {
1577
+ for (const child of value) {
1578
+ const component2 = strapi.components[child.__component];
1579
+ if (component2) await walk(strapi, child, component2, visit);
1580
+ }
1581
+ }
1582
+ }
1583
+ }
1189
1584
  const LIFETIME_SECONDS = {
1190
1585
  token: PREVIEW_TOKEN_TTL_SECONDS,
1191
- grant: PREVIEW_GRANT_TTL_SECONDS
1586
+ grant: PREVIEW_GRANT_TTL_SECONDS,
1587
+ system: PREVIEW_SYSTEM_GRANT_TTL_SECONDS
1192
1588
  };
1193
1589
  function hashCredential(value) {
1194
1590
  return createHash("sha256").update(value).digest("hex");
@@ -1196,6 +1592,9 @@ function hashCredential(value) {
1196
1592
  function issueCredential(kind2, binding, now = /* @__PURE__ */ new Date()) {
1197
1593
  const value = randomBytes(32).toString("base64url");
1198
1594
  const record = {
1595
+ adminUser: null,
1596
+ adminOrigin: null,
1597
+ entryVersion: null,
1199
1598
  ...binding,
1200
1599
  kind: kind2,
1201
1600
  hash: hashCredential(value),
@@ -1224,15 +1623,22 @@ class PreviewUnavailableError extends Error {
1224
1623
  const preview$1 = ({ strapi }) => {
1225
1624
  const store2 = () => strapi.db.query(PREVIEW_CREDENTIAL_UID);
1226
1625
  async function save(record) {
1227
- const site2 = await strapi.db.query(SITE_UID).findOne({ where: { documentId: record.siteDocumentId }, select: ["id"] });
1626
+ const site2 = await strapi.db.query(SITE_UID$1).findOne({ where: { documentId: record.siteDocumentId }, select: ["id"] });
1228
1627
  await store2().create({ data: { ...record, site: site2?.id } });
1229
1628
  }
1230
- async function find(kind2, value) {
1629
+ async function find(kinds, value) {
1231
1630
  const row = await store2().findOne({
1232
- where: { hash: hashCredential(value), kind: kind2 }
1631
+ where: { hash: hashCredential(value), kind: { $in: kinds } }
1233
1632
  });
1234
1633
  return row;
1235
1634
  }
1635
+ async function removeExpired() {
1636
+ await store2().deleteMany({
1637
+ where: {
1638
+ expiresAt: { $lt: new Date(Date.now() - PREVIEW_CREDENTIAL_RETENTION_SECONDS * 1e3) }
1639
+ }
1640
+ });
1641
+ }
1236
1642
  async function revoke(adminUser, entryDocumentId) {
1237
1643
  await store2().updateMany({
1238
1644
  where: {
@@ -1253,43 +1659,56 @@ const preview$1 = ({ strapi }) => {
1253
1659
  }
1254
1660
  return {
1255
1661
  async issueToken(binding) {
1256
- const site2 = await strapi.db.query(SITE_UID).findOne({ where: { documentId: binding.siteDocumentId }, select: ["url"] });
1662
+ const site2 = await strapi.db.query(SITE_UID$1).findOne({ where: { documentId: binding.siteDocumentId }, select: ["url"] });
1257
1663
  const url = previewUrlOf(site2?.url);
1258
1664
  if (!url) throw new PreviewUnavailableError(binding.siteDocumentId);
1259
- await store2().deleteMany({
1260
- where: {
1261
- expiresAt: { $lt: new Date(Date.now() - PREVIEW_CREDENTIAL_RETENTION_SECONDS * 1e3) }
1262
- }
1263
- });
1665
+ await removeExpired();
1264
1666
  const { value, record } = issueCredential("token", binding);
1265
1667
  await save(record);
1266
1668
  url.searchParams.set("token", value);
1267
1669
  return { url: url.toString(), expiresAt: record.expiresAt.toISOString() };
1268
1670
  },
1269
1671
  async exchange(token, siteDocumentId) {
1270
- const row = await find("token", token);
1672
+ const row = await find(["token"], token);
1271
1673
  if (!row || !mayUseCredential(toRecord(row), "token", { siteDocumentId })) return null;
1272
1674
  const claimed = await store2().updateMany({
1273
1675
  where: { id: row.id, usedAt: null, revokedAt: null },
1274
1676
  data: { usedAt: /* @__PURE__ */ new Date() }
1275
1677
  });
1276
1678
  if (claimed.count !== 1) return null;
1277
- if (!await stillReadable(row)) return null;
1278
- const { value, record } = issueCredential("grant", bindingOf(row));
1679
+ const binding = adminBindingOf(row);
1680
+ if (!binding || !await stillReadable(binding)) return null;
1681
+ const { value, record } = issueCredential("grant", binding);
1279
1682
  await save(record);
1280
1683
  return {
1281
1684
  grant: value,
1282
1685
  expiresAt: record.expiresAt.toISOString(),
1283
- adminOrigin: row.adminOrigin,
1686
+ adminOrigin: binding.adminOrigin,
1284
1687
  contentType: row.contentType,
1285
1688
  documentId: row.entryDocumentId,
1286
1689
  locale: row.entryLocale
1287
1690
  };
1288
1691
  },
1692
+ async issueSystemGrant(binding) {
1693
+ await removeExpired();
1694
+ const { value, record } = issueCredential("system", binding);
1695
+ await save(record);
1696
+ return value;
1697
+ },
1289
1698
  async resolveGrant(grant, expected) {
1290
- const row = await find("grant", grant);
1291
- if (!row || !mayUseCredential(toRecord(row), "grant", expected)) return null;
1292
- return await stillReadable(row) ? bindingOf(row) : null;
1699
+ const row = await find(["grant", "system"], grant);
1700
+ if (!row || !mayUseCredential(toRecord(row), row.kind, expected)) return null;
1701
+ if (row.kind === "system") {
1702
+ const claimed = await store2().updateMany({
1703
+ where: { id: row.id, usedAt: null, revokedAt: null },
1704
+ data: { usedAt: /* @__PURE__ */ new Date() }
1705
+ });
1706
+ if (claimed.count !== 1) return null;
1707
+ return { ...targetOf(row), actor: "system", entryVersion: row.entryVersion };
1708
+ }
1709
+ const binding = adminBindingOf(row);
1710
+ if (!binding || !await stillReadable(binding)) return null;
1711
+ return { ...targetOf(row), actor: "admin", entryVersion: null };
1293
1712
  },
1294
1713
  revoke
1295
1714
  };
@@ -1306,21 +1725,26 @@ function previewUrlOf(url) {
1306
1725
  return void 0;
1307
1726
  }
1308
1727
  }
1309
- function bindingOf(row) {
1728
+ function targetOf(row) {
1310
1729
  return {
1311
- adminUser: row.adminUser,
1312
- adminOrigin: row.adminOrigin,
1313
1730
  siteDocumentId: row.siteDocumentId,
1314
1731
  contentType: row.contentType,
1315
1732
  entryDocumentId: row.entryDocumentId,
1316
1733
  entryLocale: row.entryLocale
1317
1734
  };
1318
1735
  }
1736
+ function adminBindingOf(row) {
1737
+ if (row.adminUser === null || row.adminOrigin === null) return null;
1738
+ return { ...targetOf(row), adminUser: row.adminUser, adminOrigin: row.adminOrigin };
1739
+ }
1319
1740
  function toRecord(row) {
1320
1741
  return {
1321
- ...bindingOf(row),
1742
+ ...targetOf(row),
1322
1743
  kind: row.kind,
1323
1744
  hash: row.hash,
1745
+ adminUser: row.adminUser,
1746
+ adminOrigin: row.adminOrigin,
1747
+ entryVersion: row.entryVersion,
1324
1748
  expiresAt: new Date(row.expiresAt),
1325
1749
  usedAt: row.usedAt ? new Date(row.usedAt) : null,
1326
1750
  revokedAt: row.revokedAt ? new Date(row.revokedAt) : null
@@ -1358,6 +1782,16 @@ const preview = ({ strapi }) => {
1358
1782
  const previews = () => strapi.plugin("micelio").service("preview");
1359
1783
  const tokenLimit = createRateLimiter(PREVIEW_TOKEN_RATE_LIMIT, PREVIEW_RATE_WINDOW_SECONDS);
1360
1784
  const exchangeLimit = createRateLimiter(PREVIEW_EXCHANGE_RATE_LIMIT, PREVIEW_RATE_WINDOW_SECONDS);
1785
+ async function isVersionOf(uid, binding) {
1786
+ const current = await strapi.documents(uid).findOne({
1787
+ documentId: binding.entryDocumentId,
1788
+ locale: binding.entryLocale ?? void 0,
1789
+ status: "draft",
1790
+ fields: ["updatedAt"]
1791
+ });
1792
+ if (!current?.updatedAt || !binding.entryVersion) return false;
1793
+ return new Date(current.updatedAt).getTime() === new Date(binding.entryVersion).getTime();
1794
+ }
1361
1795
  return {
1362
1796
  async token(ctx) {
1363
1797
  const ability = ctx.state.userAbility;
@@ -1461,22 +1895,37 @@ const preview = ({ strapi }) => {
1461
1895
  }
1462
1896
  const uid = binding.contentType;
1463
1897
  const schema = strapi.contentType(uid);
1898
+ const read = readDraftQuery(strapi, query, schema);
1899
+ if ("problem" in read) {
1900
+ refuse(ctx, 400, "previewRequestInvalid", read.problem);
1901
+ return;
1902
+ }
1464
1903
  const auth = ctx.state.auth;
1465
- const params = await strapi.contentAPI.sanitize.query(query, schema, { auth });
1466
- const entry = await strapi.documents(uid).findOne({
1467
- ...params,
1468
- documentId: binding.entryDocumentId,
1469
- locale: binding.entryLocale ?? void 0,
1470
- status: "draft"
1471
- });
1472
- if (!entry) {
1904
+ let entry;
1905
+ try {
1906
+ entry = await readDraft(strapi, {
1907
+ uid,
1908
+ documentId: binding.entryDocumentId,
1909
+ locale: binding.entryLocale ?? void 0,
1910
+ populate: read.populate,
1911
+ siteDocumentId: siteOf(ctx),
1912
+ auth
1913
+ });
1914
+ } catch (error) {
1915
+ if (!(error instanceof errors.ValidationError || error instanceof errors.PaginationError)) {
1916
+ throw error;
1917
+ }
1918
+ refuse(ctx, 400, "previewRequestInvalid", error.message);
1919
+ return;
1920
+ }
1921
+ if (!entry || binding.actor === "system" && !await isVersionOf(uid, binding)) {
1473
1922
  refuse(ctx, 404, "previewRefused", REFUSED);
1474
1923
  return;
1475
1924
  }
1476
1925
  ctx.set("Cache-Control", "private, no-store");
1477
1926
  ctx.body = {
1478
1927
  data: await strapi.contentAPI.sanitize.output(entry, schema, { auth }),
1479
- meta: {}
1928
+ meta: { actor: binding.actor }
1480
1929
  };
1481
1930
  }
1482
1931
  };
@@ -1509,8 +1958,8 @@ function readTokenBody(body) {
1509
1958
  return { uid, documentId, locale };
1510
1959
  }
1511
1960
  const name = "@micelio/strapi-plugin-core";
1512
- const version$1 = "0.1.0";
1513
- const engines = { "node": ">=22.12" };
1961
+ const version$1 = "0.2.0";
1962
+ const engines = { "node": ">=22.19.0" };
1514
1963
  const micelio = "0.6";
1515
1964
  const packageJson = {
1516
1965
  name,
@@ -1755,7 +2204,7 @@ function runSanitiser(loaded, html) {
1755
2204
  return void 0;
1756
2205
  }
1757
2206
  async function readSiteOrigins(strapi) {
1758
- const sites = await strapi.db.query(SITE_UID).findMany({ select: ["url"] });
2207
+ const sites = await strapi.db.query(SITE_UID$1).findMany({ select: ["url"] });
1759
2208
  const origins = /* @__PURE__ */ new Set();
1760
2209
  for (const { url } of sites) {
1761
2210
  if (!url) continue;
@@ -1813,7 +2262,7 @@ async function readSitesWithSections(strapi, sections) {
1813
2262
  async function readSiteRows(strapi, populate) {
1814
2263
  const locales = strapi.plugin("i18n").service("locales");
1815
2264
  const defaultLocale = await locales.getDefaultLocale();
1816
- const rows = await strapi.db.query(SITE_UID).findMany({
2265
+ const rows = await strapi.db.query(SITE_UID$1).findMany({
1817
2266
  select: ["documentId", "locale", "name", "slug", "hosts", "isDefault", "url"],
1818
2267
  populate,
1819
2268
  orderBy: { slug: "asc" }
@@ -1876,7 +2325,7 @@ const extensions$1 = ({ strapi }) => {
1876
2325
  },
1877
2326
  async sites(ctx) {
1878
2327
  const ability = abilityOf2(ctx);
1879
- if (ability?.can(READ_SITES, SITE_UID) !== true) {
2328
+ if (ability?.can(READ_SITES, SITE_UID$1) !== true) {
1880
2329
  sendProblem(ctx, {
1881
2330
  type: "about:blank",
1882
2331
  title: "Forbidden",
@@ -1975,7 +2424,9 @@ function readBody(body) {
1975
2424
  return { uid, documentId, to, locale, publishAt };
1976
2425
  }
1977
2426
  const controllers = {
1978
- site: factories.createCoreController(SITE_UID),
2427
+ site: factories.createCoreController(SITE_UID$1),
2428
+ category: factories.createCoreController(CATEGORY_UID),
2429
+ tag: factories.createCoreController(TAG_UID),
1979
2430
  blocks,
1980
2431
  transitions: transitions$1,
1981
2432
  status: status$1,
@@ -2015,7 +2466,7 @@ const siteScope = (_config, { strapi }) => {
2015
2466
  };
2016
2467
  };
2017
2468
  async function loadSites(strapi) {
2018
- const rows = await strapi.db.query(SITE_UID).findMany({
2469
+ const rows = await strapi.db.query(SITE_UID$1).findMany({
2019
2470
  select: ["documentId", "slug", "hosts", "isDefault"]
2020
2471
  });
2021
2472
  const sites = /* @__PURE__ */ new Map();
@@ -2117,16 +2568,16 @@ const COMPONENTS = {
2117
2568
  function registerComponents(strapi) {
2118
2569
  const components = Object.fromEntries(
2119
2570
  Object.entries(COMPONENTS).flatMap(
2120
- ([category, schemas]) => Object.entries(schemas).map(([name2, schema]) => {
2121
- const uid = `${category}.${name2}`;
2571
+ ([category2, schemas]) => Object.entries(schemas).map(([name2, schema]) => {
2572
+ const uid = `${category2}.${name2}`;
2122
2573
  const component2 = {
2123
2574
  ...structuredClone(schema),
2124
2575
  __schema__: structuredClone(schema),
2125
2576
  uid,
2126
- category,
2577
+ category: category2,
2127
2578
  modelType: "component",
2128
2579
  modelName: name2,
2129
- globalId: `Component${pascalCase(category)}${pascalCase(name2)}`
2580
+ globalId: `Component${pascalCase(category2)}${pascalCase(name2)}`
2130
2581
  };
2131
2582
  return [uid, component2];
2132
2583
  })
@@ -2174,10 +2625,120 @@ async function storedState(strapi, uid, params) {
2174
2625
  });
2175
2626
  return entry?.editorial?.state ?? "draft";
2176
2627
  }
2628
+ const LEGACY_TO_PLUGIN = {
2629
+ "api::category.category": "plugin::micelio.category",
2630
+ "api::tag.tag": "plugin::micelio.tag"
2631
+ };
2632
+ const CONFIGURATION_KEY_PREFIX = "plugin_content_manager_configuration_content_types::";
2633
+ const ACTION_TABLES = [
2634
+ { uid: "plugin::users-permissions.permission", owner: "role" },
2635
+ { uid: "admin::api-token-permission", owner: "token" }
2636
+ ];
2637
+ async function renameLegacyUids(strapi) {
2638
+ const report = { actions: 0, subjects: 0, layouts: 0 };
2639
+ for (const [legacy, current] of Object.entries(LEGACY_TO_PLUGIN)) {
2640
+ if (strapi.contentTypes[legacy]) {
2641
+ strapi.log.info(
2642
+ `micelio: kept the grants and layouts of ${legacy}: the host still registers that type`
2643
+ );
2644
+ continue;
2645
+ }
2646
+ for (const { uid, owner } of ACTION_TABLES) {
2647
+ report.actions += await renameActions(strapi, uid, owner, legacy, current);
2648
+ }
2649
+ report.subjects += await renameSubjects(strapi, legacy, current);
2650
+ report.layouts += await renameLayouts(strapi, legacy, current);
2651
+ }
2652
+ return report;
2653
+ }
2654
+ async function hasTable(strapi, uid) {
2655
+ if (!strapi.db.metadata.has(uid)) return false;
2656
+ const tableName = strapi.db.metadata.get(uid).tableName;
2657
+ return Boolean(tableName) && await strapi.db.getSchemaConnection().hasTable(tableName);
2658
+ }
2659
+ async function renameActions(strapi, uid, owner, legacy, current) {
2660
+ if (!await hasTable(strapi, uid)) return 0;
2661
+ const rows = await strapi.db.query(uid).findMany({
2662
+ where: { action: { $startsWith: `${legacy}.` } },
2663
+ populate: { [owner]: { select: ["id"] } }
2664
+ });
2665
+ for (const row of rows) {
2666
+ const action = `${current}${row.action.slice(legacy.length)}`;
2667
+ const ownerRow = row[owner];
2668
+ const twin = ownerRow ? await strapi.db.query(uid).findOne({ where: { action, [owner]: ownerRow.id } }) : null;
2669
+ if (twin) {
2670
+ await strapi.db.query(uid).delete({ where: { id: row.id } });
2671
+ } else {
2672
+ await strapi.db.query(uid).update({ where: { id: row.id }, data: { action } });
2673
+ }
2674
+ }
2675
+ return rows.length;
2676
+ }
2677
+ async function renameSubjects(strapi, legacy, current) {
2678
+ const uid = "admin::permission";
2679
+ if (!await hasTable(strapi, uid)) return 0;
2680
+ const rows = await strapi.db.query(uid).findMany({
2681
+ where: { subject: legacy },
2682
+ populate: { role: { select: ["id"] } }
2683
+ });
2684
+ for (const row of rows) {
2685
+ const twin = row.role ? await strapi.db.query(uid).findOne({ where: { action: row.action, subject: current, role: row.role.id } }) : null;
2686
+ if (twin) {
2687
+ await strapi.db.query(uid).delete({ where: { id: row.id } });
2688
+ } else {
2689
+ await strapi.db.query(uid).update({ where: { id: row.id }, data: { subject: current } });
2690
+ }
2691
+ }
2692
+ return rows.length;
2693
+ }
2694
+ function replaceWholeUid(text2, legacy, current) {
2695
+ const escaped = legacy.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2696
+ return text2.replace(new RegExp(`${escaped}(?![A-Za-z0-9_-])`, "g"), () => current);
2697
+ }
2698
+ async function renameLayouts(strapi, legacy, current) {
2699
+ const uid = "strapi::core-store";
2700
+ if (!await hasTable(strapi, uid)) return 0;
2701
+ const rows = await strapi.db.query(uid).findMany({
2702
+ where: { key: { $startsWith: CONFIGURATION_KEY_PREFIX } }
2703
+ });
2704
+ let changed = 0;
2705
+ for (const row of rows) {
2706
+ const key = replaceWholeUid(row.key, legacy, current);
2707
+ const value = row.value ? replaceWholeUid(row.value, legacy, current) : row.value;
2708
+ if (key === row.key && value === row.value) continue;
2709
+ changed += 1;
2710
+ if (key !== row.key && await strapi.db.query(uid).findOne({ where: { key } })) {
2711
+ await strapi.db.query(uid).delete({ where: { id: row.id } });
2712
+ } else {
2713
+ await strapi.db.query(uid).update({ where: { id: row.id }, data: { key, value } });
2714
+ }
2715
+ }
2716
+ return changed;
2717
+ }
2718
+ function previewPageUrl(strapi, uid, { documentId, locale }) {
2719
+ const schema = strapi.contentType(uid);
2720
+ const hasDynamicZone = Object.values(schema?.attributes ?? {}).some(
2721
+ ({ type }) => type === "dynamiczone"
2722
+ );
2723
+ if (!documentId || schema?.kind !== "collectionType") return void 0;
2724
+ if (!isSiteScoped(schema) || !hasDynamicZone) return void 0;
2725
+ const base = strapi.config.get("admin.url", "/admin");
2726
+ const page = `${base.replace(/\/$/, "")}/plugins/micelio/preview/${encodeURIComponent(uid)}/${encodeURIComponent(documentId)}`;
2727
+ return locale ? `${page}?locale=${encodeURIComponent(locale)}` : page;
2728
+ }
2729
+ function registerPreviewShortcut(strapi) {
2730
+ if (strapi.config.get("admin.preview") !== void 0) return;
2731
+ strapi.config.set("admin.preview", {
2732
+ enabled: true,
2733
+ config: {
2734
+ handler: (uid, params) => previewPageUrl(strapi, uid, params)
2735
+ }
2736
+ });
2737
+ }
2177
2738
  function siteCreatedHook(strapi) {
2178
2739
  return async (context, next) => {
2179
2740
  const result = await next();
2180
- if (context.uid !== SITE_UID || context.action !== "create") return result;
2741
+ if (context.uid !== SITE_UID$1 || context.action !== "create") return result;
2181
2742
  const site2 = result;
2182
2743
  await hooksOf(strapi).runAfter("siteCreated", {
2183
2744
  site: site2.documentId,
@@ -2196,14 +2757,16 @@ function siteScopeDocuments(strapi) {
2196
2757
  const site2 = currentSite(strapi);
2197
2758
  if (context.action === "create") {
2198
2759
  params.data = params.data ?? {};
2199
- if (isMissing(params.data[SITE_ATTRIBUTE])) {
2200
- if (!site2) throw new errors.ValidationError(`${SITE_ATTRIBUTE} is required`);
2201
- params.data[SITE_ATTRIBUTE] = site2.documentId;
2760
+ if (site2) params.data[SITE_ATTRIBUTE$1] = site2.documentId;
2761
+ else if (isMissing(params.data[SITE_ATTRIBUTE$1])) {
2762
+ throw new errors.ValidationError(`${SITE_ATTRIBUTE$1} is required`);
2202
2763
  }
2203
2764
  }
2204
- if (context.action === "update" && params.data && SITE_ATTRIBUTE in params.data) {
2205
- if (isMissing(params.data[SITE_ATTRIBUTE])) {
2206
- throw new errors.ValidationError(`${SITE_ATTRIBUTE} is required`);
2765
+ if (context.action === "update" && params.data && SITE_ATTRIBUTE$1 in params.data) {
2766
+ if (site2) delete params.data[SITE_ATTRIBUTE$1];
2767
+ else if (isUnchanged(params.data[SITE_ATTRIBUTE$1])) delete params.data[SITE_ATTRIBUTE$1];
2768
+ else if (isMissing(params.data[SITE_ATTRIBUTE$1])) {
2769
+ throw new errors.ValidationError(`${SITE_ATTRIBUTE$1} is required`);
2207
2770
  }
2208
2771
  }
2209
2772
  if (site2 && READ_ACTIONS.has(context.action)) {
@@ -2215,21 +2778,119 @@ function siteScopeDocuments(strapi) {
2215
2778
  });
2216
2779
  if (owned === 0) throw new errors.NotFoundError("Not Found");
2217
2780
  }
2218
- return next();
2781
+ if (context.action === "update" && params.documentId) {
2782
+ await keepSiteOfLocales(strapi, context.uid, params);
2783
+ }
2784
+ const result = await next();
2785
+ if (rebuildsRelations(context.action, params) && params.documentId) {
2786
+ await repairSiteOfLocales(strapi, context.uid, params);
2787
+ }
2788
+ return result;
2219
2789
  };
2220
2790
  }
2791
+ async function keepSiteOfLocales(strapi, uid, params) {
2792
+ if (!isLocalized(strapi, uid)) return;
2793
+ const locale = typeof params.locale === "string" ? params.locale : await strapi.plugin("i18n").service("locales").getDefaultLocale();
2794
+ const { documentId } = params;
2795
+ const rows = await strapi.db.query(uid).findMany({
2796
+ where: { documentId },
2797
+ populate: { [SITE_ATTRIBUTE$1]: { select: ["id", "documentId"] } }
2798
+ });
2799
+ if (rows.length === 0) return;
2800
+ const named = params.data?.[SITE_ATTRIBUTE$1];
2801
+ if (rows.some((row) => row.locale === locale)) {
2802
+ if (named !== void 0) await refuseMoveAwayFromOtherLocales(strapi, rows, locale, named);
2803
+ return;
2804
+ }
2805
+ const sites = new Map(
2806
+ rows.flatMap((row) => row.site ? [[row.site.documentId, row.site.id]] : [])
2807
+ );
2808
+ const data = params.data = params.data ?? {};
2809
+ if (named !== void 0) {
2810
+ if (sites.size !== 1) return;
2811
+ const [[shared]] = [...sites];
2812
+ for (const documentIdOfNamed of await namedSites(strapi, named)) {
2813
+ if (documentIdOfNamed !== shared) {
2814
+ throw new errors.ValidationError(`${SITE_ATTRIBUTE$1} must be the site of the other locales`);
2815
+ }
2816
+ }
2817
+ } else if (sites.size !== 1) {
2818
+ throw new errors.ValidationError(`${SITE_ATTRIBUTE$1} is required`);
2819
+ }
2820
+ data[SITE_ATTRIBUTE$1] = { connect: [{ id: [...sites.values()][0] }] };
2821
+ }
2822
+ async function refuseMoveAwayFromOtherLocales(strapi, rows, locale, named) {
2823
+ const target = new Set(await namedSites(strapi, named));
2824
+ const own = rows.filter((row) => row.locale === locale).flatMap((row) => row.site?.documentId ?? []);
2825
+ if (target.size === 0 || [...target].every((site2) => own.includes(site2))) return;
2826
+ const staysElsewhere = rows.some(
2827
+ (row) => row.locale !== locale && row.site && !target.has(row.site.documentId)
2828
+ );
2829
+ if (staysElsewhere) {
2830
+ throw new errors.ValidationError(
2831
+ `${SITE_ATTRIBUTE$1} cannot change while other locales of the document stay on another site`
2832
+ );
2833
+ }
2834
+ }
2835
+ async function namedSites(strapi, value) {
2836
+ if (typeof value === "string") return [value];
2837
+ if (typeof value === "number") return [await documentIdOfSite(strapi, value)];
2838
+ if (Array.isArray(value))
2839
+ return (await Promise.all(value.map((v) => namedSites(strapi, v)))).flat();
2840
+ if (typeof value !== "object" || value === null) return [];
2841
+ const { documentId, id: id2, connect, set } = value;
2842
+ if (typeof documentId === "string") return [documentId];
2843
+ if (typeof id2 === "number") return [await documentIdOfSite(strapi, id2)];
2844
+ return [...await namedSites(strapi, connect), ...await namedSites(strapi, set)];
2845
+ }
2846
+ async function documentIdOfSite(strapi, id2) {
2847
+ const row = await strapi.db.query(SITE_UID$1).findOne({ where: { id: id2 } });
2848
+ return row?.documentId ?? String(id2);
2849
+ }
2850
+ function isLocalized(strapi, uid) {
2851
+ const model = strapi.contentType(uid);
2852
+ const options2 = model.pluginOptions;
2853
+ return options2?.i18n?.localized === true;
2854
+ }
2855
+ function rebuildsRelations(action, params) {
2856
+ return action === "publish" || action === "discardDraft" || action === "update" && params.status === "published";
2857
+ }
2858
+ async function repairSiteOfLocales(strapi, uid, params) {
2859
+ if (!isLocalized(strapi, uid)) return;
2860
+ const query = strapi.db.query(uid);
2861
+ const rows = await query.findMany({
2862
+ where: {
2863
+ documentId: params.documentId,
2864
+ ...typeof params.locale === "string" && params.locale !== "*" ? { locale: params.locale } : {}
2865
+ },
2866
+ populate: { [SITE_ATTRIBUTE$1]: { select: ["id"] } }
2867
+ });
2868
+ for (const row of rows.filter((candidate) => !candidate.site)) {
2869
+ const other = rows.find((candidate) => candidate.locale === row.locale && candidate.site);
2870
+ if (other?.site) {
2871
+ await query.update({ where: { id: row.id }, data: { [SITE_ATTRIBUTE$1]: other.site.id } });
2872
+ }
2873
+ }
2874
+ }
2221
2875
  function currentSite(strapi) {
2222
2876
  const state = strapi.requestContext.get()?.state;
2223
2877
  return state?.micelio?.site;
2224
2878
  }
2225
2879
  function siteFilter(site2) {
2226
- return { [SITE_ATTRIBUTE]: { documentId: { $eq: site2.documentId } } };
2880
+ return { [SITE_ATTRIBUTE$1]: { documentId: { $eq: site2.documentId } } };
2881
+ }
2882
+ function isUnchanged(value) {
2883
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2884
+ const { connect, disconnect, ...rest } = value;
2885
+ const isEmpty = (list2) => list2 === void 0 || Array.isArray(list2) && list2.length === 0;
2886
+ return Object.keys(rest).length === 0 && isEmpty(connect) && isEmpty(disconnect);
2227
2887
  }
2228
2888
  function isMissing(value) {
2229
2889
  if (value === void 0 || value === null || value === "") return true;
2230
2890
  if (Array.isArray(value)) return value.length === 0;
2231
2891
  if (typeof value === "object") {
2232
- const { connect, set } = value;
2892
+ const { connect, set, documentId, id: id2 } = value;
2893
+ if (documentId !== void 0 || id2 !== void 0) return !(documentId || id2);
2233
2894
  const list2 = set ?? connect;
2234
2895
  return Array.isArray(list2) ? list2.length === 0 : list2 === void 0;
2235
2896
  }
@@ -2250,7 +2911,7 @@ function normalizeHosts(value) {
2250
2911
  }
2251
2912
  return hosts;
2252
2913
  }
2253
- const SITE = SITE_UID;
2914
+ const SITE = SITE_UID$1;
2254
2915
  function siteRules(strapi) {
2255
2916
  return async (context, next) => {
2256
2917
  if (context.uid !== SITE) return next();
@@ -2299,6 +2960,13 @@ async function hostsOfOtherSites(strapi, documentId) {
2299
2960
  }
2300
2961
  const register = ({ strapi }) => {
2301
2962
  registerComponents(strapi);
2963
+ registerPreviewShortcut(strapi);
2964
+ strapi.hook("strapi::content-types.beforeSync").register(async () => {
2965
+ const report = await renameLegacyUids(strapi);
2966
+ if (report.actions + report.subjects + report.layouts > 0) {
2967
+ strapi.log.info(`[micelio] moved legacy category and tag UIDs: ${JSON.stringify(report)}`);
2968
+ }
2969
+ });
2302
2970
  strapi.documents.use(siteCreatedHook(strapi));
2303
2971
  strapi.documents.use(siteRules(strapi));
2304
2972
  strapi.documents.use(siteSectionInstances(strapi));
@@ -2375,14 +3043,32 @@ const contentApiRoutes = () => ({
2375
3043
  }
2376
3044
  ]
2377
3045
  });
2378
- const siteRoutes = factories.createCoreRouter(SITE_UID, {
3046
+ const siteRoutes = factories.createCoreRouter(SITE_UID$1, {
2379
3047
  prefix: "/micelio",
2380
3048
  only: ["find", "findOne"]
2381
3049
  });
3050
+ const scoped = () => ({ middlewares: [SITE_SCOPE_MIDDLEWARE] });
3051
+ const config = () => ({
3052
+ find: scoped(),
3053
+ findOne: scoped(),
3054
+ create: scoped(),
3055
+ update: scoped(),
3056
+ delete: scoped()
3057
+ });
3058
+ const categoryRoutes = factories.createCoreRouter(CATEGORY_UID, {
3059
+ prefix: "",
3060
+ config: config()
3061
+ });
3062
+ const tagRoutes = factories.createCoreRouter(TAG_UID, {
3063
+ prefix: "",
3064
+ config: config()
3065
+ });
2382
3066
  const routes = {
2383
3067
  "content-api": contentApiRoutes,
2384
3068
  admin: adminRoutes,
2385
- site: siteRoutes
3069
+ site: siteRoutes,
3070
+ category: categoryRoutes,
3071
+ tag: tagRoutes
2386
3072
  };
2387
3073
  const extensions = ({ strapi }) => createExtensionRegistry(strapi);
2388
3074
  const hooks = ({ strapi }) => createHookRegistry(strapi.log);
@@ -2625,7 +3311,7 @@ function asList(value) {
2625
3311
  function exportedTypes(strapi, personal) {
2626
3312
  const types = [];
2627
3313
  for (const [uid, schema] of Object.entries(strapi.contentTypes)) {
2628
- if (uid !== SITE_UID && !isSiteScoped(schema)) continue;
3314
+ if (uid !== SITE_UID$1 && !isSiteScoped(schema)) continue;
2629
3315
  if (!isWritten(exportPolicyOf(schema), personal)) continue;
2630
3316
  types.push({
2631
3317
  uid,
@@ -2641,20 +3327,20 @@ async function siteRowsOf(strapi, type, site2, context) {
2641
3327
  const query = strapi.db.query(type.uid);
2642
3328
  const owned = await query.findMany({
2643
3329
  select: ["documentId"],
2644
- where: { [SITE_ATTRIBUTE]: { documentId: site2.documentId } }
3330
+ where: { [SITE_ATTRIBUTE$1]: { documentId: site2.documentId } }
2645
3331
  });
2646
3332
  const documentIds = [...new Set(owned.map((row) => row.documentId))];
2647
3333
  if (documentIds.length === 0) return [];
2648
3334
  return await query.findMany({
2649
3335
  where: { documentId: { $in: documentIds } },
2650
- populate: populateOf(type.attributes, context, /* @__PURE__ */ new Set([SITE_ATTRIBUTE]))
3336
+ populate: populateOf(type.attributes, context, /* @__PURE__ */ new Set([SITE_ATTRIBUTE$1]))
2651
3337
  });
2652
3338
  }
2653
3339
  function attributesOf(strapi, uid) {
2654
3340
  return strapi.contentType(uid).attributes;
2655
3341
  }
2656
3342
  function documentsOf(type, rows, context) {
2657
- const skip = new Set(type.uid === SITE_UID ? [] : [SITE_ATTRIBUTE]);
3343
+ const skip = new Set(type.uid === SITE_UID$1 ? [] : [SITE_ATTRIBUTE$1]);
2658
3344
  const documents = /* @__PURE__ */ new Map();
2659
3345
  for (const row of rows) {
2660
3346
  const version2 = {
@@ -2864,15 +3550,15 @@ async function exportSite(strapi, request, output) {
2864
3550
  },
2865
3551
  personal
2866
3552
  };
2867
- const siteRows = await strapi.db.query(SITE_UID).findMany({
3553
+ const siteRows = await strapi.db.query(SITE_UID$1).findMany({
2868
3554
  where: { slug: request.site },
2869
- populate: populateOf(attributesOf(strapi, SITE_UID), context)
3555
+ populate: populateOf(attributesOf(strapi, SITE_UID$1), context)
2870
3556
  });
2871
3557
  const site2 = siteRows[0];
2872
3558
  if (!site2) throw new ExportError("exportSiteNotFound", `There is no site "${request.site}"`);
2873
3559
  const contents = [];
2874
3560
  for (const type of types) {
2875
- const rows = type.uid === SITE_UID ? siteRows : await siteRowsOf(strapi, type, site2, context);
3561
+ const rows = type.uid === SITE_UID$1 ? siteRows : await siteRowsOf(strapi, type, site2, context);
2876
3562
  contents.push({ type: type.name, documents: documentsOf(type, rows, context) });
2877
3563
  }
2878
3564
  const index2 = await mediaIndex(strapi, media2);
@@ -3408,7 +4094,7 @@ function all(types, strapi) {
3408
4094
  async function targetSite(strapi, staged, request) {
3409
4095
  const archived = staged.contents.get("site").documents[0];
3410
4096
  const data = archived.versions[0].data;
3411
- const rows = await strapi.db.query(SITE_UID).findMany({
4097
+ const rows = await strapi.db.query(SITE_UID$1).findMany({
3412
4098
  select: ["documentId", "slug", "hosts", "isDefault"]
3413
4099
  });
3414
4100
  const sites = [...new Map(rows.map((row) => [row.documentId, row])).values()];
@@ -3702,7 +4388,7 @@ async function writeContent(strapi, plan2, files, request, report) {
3702
4388
  const context = deserialiseContext(strapi, plan2, files, written);
3703
4389
  const shaOf = new Map([...files.entries()].map(([sha256, id2]) => [id2, sha256]));
3704
4390
  const ordered = [...plan2.types].sort(
3705
- (a, b) => a.uid === SITE_UID ? -1 : b.uid === SITE_UID ? 1 : compare(a.name, b.name)
4391
+ (a, b) => a.uid === SITE_UID$1 ? -1 : b.uid === SITE_UID$1 ? 1 : compare(a.name, b.name)
3706
4392
  );
3707
4393
  const pending = [];
3708
4394
  for (const type of ordered) {
@@ -3718,7 +4404,7 @@ async function writeContent(strapi, plan2, files, request, report) {
3718
4404
  }
3719
4405
  if (request.mode === "replace") {
3720
4406
  for (const type of ordered) {
3721
- if (type.uid === SITE_UID) continue;
4407
+ if (type.uid === SITE_UID$1) continue;
3722
4408
  const kept = new Set(plan2.staged.contents.get(type.name).documents.map((d) => d.documentId));
3723
4409
  const rows = await siteRowsOf(strapi, type, plan2.site, serialiseContext(strapi, plan2, shaOf));
3724
4410
  for (const documentId of new Set(rows.map((row) => row.documentId))) {
@@ -3730,9 +4416,9 @@ async function writeContent(strapi, plan2, files, request, report) {
3730
4416
  }
3731
4417
  }
3732
4418
  async function writeDocument(strapi, plan2, type, document, context, shaOf) {
3733
- const isSite = type.uid === SITE_UID;
4419
+ const isSite = type.uid === SITE_UID$1;
3734
4420
  const documentId = isSite ? plan2.site.documentId : document.documentId;
3735
- const skip = new Set(isSite ? [] : [SITE_ATTRIBUTE]);
4421
+ const skip = new Set(isSite ? [] : [SITE_ATTRIBUTE$1]);
3736
4422
  const query = strapi.db.query(type.uid);
3737
4423
  const rows = await query.findMany({
3738
4424
  where: { documentId },
@@ -3755,7 +4441,7 @@ async function writeDocument(strapi, plan2, type, document, context, shaOf) {
3755
4441
  pending ||= result.pending > 0;
3756
4442
  const data = { ...result.data };
3757
4443
  if (!exists) {
3758
- if (!isSite) data[SITE_ATTRIBUTE] = plan2.site.documentId;
4444
+ if (!isSite) data[SITE_ATTRIBUTE$1] = plan2.site.documentId;
3759
4445
  await documents.create({ locale, data: { ...data, documentId } });
3760
4446
  exists = true;
3761
4447
  } else {
@@ -3808,7 +4494,7 @@ async function writeDocument(strapi, plan2, type, document, context, shaOf) {
3808
4494
  }
3809
4495
  function desiredData(type, version2, plan2, rows) {
3810
4496
  const data = knownData(type.attributes, version2.data);
3811
- if (type.uid === SITE_UID) {
4497
+ if (type.uid === SITE_UID$1) {
3812
4498
  data.isDefault = rows[0]?.isDefault === true;
3813
4499
  if (!plan2.site.isNew) data.slug = plan2.site.slug;
3814
4500
  }
@@ -3817,7 +4503,7 @@ function desiredData(type, version2, plan2, rows) {
3817
4503
  async function checkOwner(strapi, type, documentId, site2) {
3818
4504
  const rows = await strapi.db.query(type.uid).findMany({
3819
4505
  where: { documentId },
3820
- populate: { [SITE_ATTRIBUTE]: { select: ["documentId"] } }
4506
+ populate: { [SITE_ATTRIBUTE$1]: { select: ["documentId"] } }
3821
4507
  });
3822
4508
  if (rows.some((row) => row.site?.documentId && row.site.documentId !== site2)) {
3823
4509
  throw new ExportError(
@@ -3895,7 +4581,9 @@ const transitions = ({ strapi }) => ({
3895
4581
  }
3896
4582
  });
3897
4583
  const services = {
3898
- site: factories.createCoreService(SITE_UID),
4584
+ site: factories.createCoreService(SITE_UID$1),
4585
+ category: factories.createCoreService(CATEGORY_UID),
4586
+ tag: factories.createCoreService(TAG_UID),
3899
4587
  transitions,
3900
4588
  hostCheck,
3901
4589
  hooks,
@@ -3908,7 +4596,7 @@ const index = {
3908
4596
  register,
3909
4597
  bootstrap,
3910
4598
  destroy,
3911
- config,
4599
+ config: config$1,
3912
4600
  controllers,
3913
4601
  routes,
3914
4602
  services,
@@ -3917,10 +4605,13 @@ const index = {
3917
4605
  middlewares
3918
4606
  };
3919
4607
  export {
4608
+ CATEGORY_UID,
3920
4609
  EDITORIAL_STATES,
3921
- SITE_ATTRIBUTE,
4610
+ READ_ACTIONS$1 as READ_ACTIONS,
4611
+ SITE_ATTRIBUTE$1 as SITE_ATTRIBUTE,
3922
4612
  SITE_SCOPE_MIDDLEWARE,
3923
- SITE_UID,
4613
+ SITE_UID$1 as SITE_UID,
4614
+ TAG_UID,
3924
4615
  index as default,
3925
4616
  formatHostCheckReport,
3926
4617
  missingLocales,