@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
@@ -4,9 +4,9 @@ const zod = require("zod");
4
4
  const node_async_hooks = require("node:async_hooks");
5
5
  const node_crypto = require("node:crypto");
6
6
  const strapi = require("@strapi/strapi");
7
+ const utils = require("@strapi/utils");
7
8
  const node_module = require("node:module");
8
9
  const path = require("node:path");
9
- const utils = require("@strapi/utils");
10
10
  const node_stream = require("node:stream");
11
11
  const promises = require("node:stream/promises");
12
12
  const node_zlib = require("node:zlib");
@@ -17,7 +17,7 @@ const node_events = require("node:events");
17
17
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
18
18
  const path__default = /* @__PURE__ */ _interopDefault(path);
19
19
  const os__default = /* @__PURE__ */ _interopDefault(os);
20
- const SITE_UID = "plugin::micelio.site";
20
+ const SITE_UID$1 = "plugin::micelio.site";
21
21
  const SITE_COMPONENT_CATEGORY = "micelio-site";
22
22
  const DEFAULT_SITE_SLUG = "default";
23
23
  const LOCALISED_LOCALES = ["en", "es"];
@@ -118,7 +118,7 @@ function parseExportSection(value) {
118
118
  function missingLocales(value) {
119
119
  return LOCALISED_LOCALES.filter((locale) => !value[locale]?.trim());
120
120
  }
121
- const attributeOf = (id2) => `${SITE_SECTION_ATTRIBUTE_PREFIX}${id2.replace(/-/g, "_")}`;
121
+ const attributeOf$1 = (id2) => `${SITE_SECTION_ATTRIBUTE_PREFIX}${id2.replace(/-/g, "_")}`;
122
122
  function createExtensionRegistry(strapi2) {
123
123
  const pages = [];
124
124
  const sections = [];
@@ -155,8 +155,8 @@ function createExtensionRegistry(strapi2) {
155
155
  "micelio: registerSiteSection: call it in `register`, before the models are built"
156
156
  );
157
157
  }
158
- const attribute = attributeOf(section.id);
159
- strapi2.get("content-types").extend(SITE_UID, (contentType) => {
158
+ const attribute = attributeOf$1(section.id);
159
+ strapi2.get("content-types").extend(SITE_UID$1, (contentType) => {
160
160
  Object.assign(contentType.attributes, {
161
161
  [attribute]: {
162
162
  type: "component",
@@ -185,7 +185,7 @@ function createExtensionRegistry(strapi2) {
185
185
  siteSections: () => [...sections],
186
186
  statusChecks: () => [...checks],
187
187
  exportSections: () => [...exports2],
188
- siteSectionAttribute: attributeOf,
188
+ siteSectionAttribute: attributeOf$1,
189
189
  async runStatusChecks(sites) {
190
190
  for (const registration of checks) {
191
191
  const found = [];
@@ -209,12 +209,12 @@ function createExtensionRegistry(strapi2) {
209
209
  };
210
210
  }
211
211
  function hasDatabaseModels(strapi2) {
212
- return strapi2.db.metadata.has(SITE_UID);
212
+ return strapi2.db.metadata.has(SITE_UID$1);
213
213
  }
214
214
  function extensionsOf(strapi2) {
215
215
  return strapi2.plugin("micelio").service("extensions");
216
216
  }
217
- const SITE$1 = SITE_UID;
217
+ const SITE$1 = SITE_UID$1;
218
218
  function siteSectionInstances(strapi2) {
219
219
  return async (context, next) => {
220
220
  if (context.uid === SITE$1 && context.action === "create") {
@@ -347,7 +347,7 @@ function deepFreeze(value) {
347
347
  }
348
348
  return value;
349
349
  }
350
- const SITE_ATTRIBUTE = "site";
350
+ const SITE_ATTRIBUTE$1 = "site";
351
351
  const SITE_SCOPE_MIDDLEWARE = "plugin::micelio.site-scope";
352
352
  function withSiteScope(schema) {
353
353
  return {
@@ -358,10 +358,10 @@ function withSiteScope(schema) {
358
358
  },
359
359
  attributes: {
360
360
  ...schema.attributes,
361
- [SITE_ATTRIBUTE]: {
361
+ [SITE_ATTRIBUTE$1]: {
362
362
  type: "relation",
363
363
  relation: "manyToOne",
364
- target: SITE_UID,
364
+ target: SITE_UID$1,
365
365
  required: true,
366
366
  pluginOptions: { i18n: { localized: false } }
367
367
  }
@@ -386,7 +386,8 @@ const TRANSITIONS = [
386
386
  { from: "draft", to: "scheduled" },
387
387
  { from: "scheduled", to: "draft" },
388
388
  { from: "scheduled", to: "published" },
389
- { from: "published", to: "draft" }
389
+ { from: "published", to: "draft" },
390
+ { from: "published", to: "published" }
390
391
  ];
391
392
  const SYSTEM_TRANSITIONS = /* @__PURE__ */ new Set(["scheduled->published"]);
392
393
  const SUPER_ADMIN = "strapi-super-admin";
@@ -398,7 +399,8 @@ const DEFAULT_EDITORIAL_POLICY = {
398
399
  "in_review->published": [EDITOR, SUPER_ADMIN],
399
400
  "draft->scheduled": [EDITOR, SUPER_ADMIN],
400
401
  "scheduled->draft": [EDITOR, SUPER_ADMIN],
401
- "published->draft": [EDITOR, SUPER_ADMIN]
402
+ "published->draft": [EDITOR, SUPER_ADMIN],
403
+ "published->published": [EDITOR, SUPER_ADMIN]
402
404
  };
403
405
  function transitionKey({ from, to }) {
404
406
  return `${from}->${to}`;
@@ -454,7 +456,7 @@ async function performTransition(strapi2, request) {
454
456
  status: "draft",
455
457
  populate: {
456
458
  [EDITORIAL_ATTRIBUTE]: { populate: ["reviewedBy", "publishedBy"] },
457
- [SITE_ATTRIBUTE]: { fields: ["documentId", "editorialPolicy"] },
459
+ [SITE_ATTRIBUTE$1]: { fields: ["documentId", "editorialPolicy"] },
458
460
  createdBy: true
459
461
  }
460
462
  });
@@ -513,7 +515,9 @@ async function performTransition(strapi2, request) {
513
515
  throw new TransitionError(409, "editorialTransitionRefused", refusalMessage(error));
514
516
  }
515
517
  await runAsTransition(async () => {
516
- if (transition.from === "published") await documents.unpublish(target);
518
+ if (transition.from === "published" && transition.to !== "published") {
519
+ await documents.unpublish(target);
520
+ }
517
521
  await documents.update({ ...target, data: { [EDITORIAL_ATTRIBUTE]: editorial2 } });
518
522
  if (transition.to === "published") await documents.publish(target);
519
523
  });
@@ -1040,10 +1044,10 @@ async function storedFingerprint(strapi2) {
1040
1044
  }
1041
1045
  const SITE_LOCALES = ["en", "es"];
1042
1046
  async function ensureDefaultSite(strapi2) {
1043
- if (await strapi2.db.query(SITE_UID).count() > 0) return false;
1047
+ if (await strapi2.db.query(SITE_UID$1).count() > 0) return false;
1044
1048
  const locales = strapi2.plugin("i18n").service("locales");
1045
1049
  const locale = await locales.getDefaultLocale();
1046
- await strapi2.documents(SITE_UID).create({
1050
+ await strapi2.documents(SITE_UID$1).create({
1047
1051
  locale,
1048
1052
  data: {
1049
1053
  name: "Micelio",
@@ -1057,6 +1061,28 @@ async function ensureDefaultSite(strapi2) {
1057
1061
  });
1058
1062
  return true;
1059
1063
  }
1064
+ const SITE_UID = "plugin::micelio.site";
1065
+ const SITE_ATTRIBUTE = "site";
1066
+ const BACKFILLED = ["plugin::micelio.category", "plugin::micelio.tag"];
1067
+ async function backfillSite(strapi2) {
1068
+ const site2 = await strapi2.db.query(SITE_UID).findOne({
1069
+ where: { isDefault: true },
1070
+ select: ["id"]
1071
+ });
1072
+ if (!site2) return 0;
1073
+ let linked = 0;
1074
+ for (const uid of BACKFILLED) {
1075
+ const rows = await strapi2.db.query(uid).findMany({
1076
+ where: { [SITE_ATTRIBUTE]: null },
1077
+ select: ["id"]
1078
+ });
1079
+ for (const row of rows) {
1080
+ await strapi2.db.query(uid).update({ where: { id: row.id }, data: { [SITE_ATTRIBUTE]: site2.id } });
1081
+ }
1082
+ linked += rows.length;
1083
+ }
1084
+ return linked;
1085
+ }
1060
1086
  async function registerPreviewPermission(strapi2) {
1061
1087
  await strapi2.service("admin::permission").actionProvider.registerMany([
1062
1088
  {
@@ -1067,9 +1093,13 @@ async function registerPreviewPermission(strapi2) {
1067
1093
  }
1068
1094
  ]);
1069
1095
  }
1096
+ const ADMIN_LOGOUT_HANDLER = "authentication.logout";
1070
1097
  function registerPreviewRevocation(strapi2) {
1071
- strapi2.eventHub.on("admin.logout", async (event) => {
1072
- const id2 = event.user?.id;
1098
+ strapi2.server.use(async (ctx, next) => {
1099
+ await next();
1100
+ const route = ctx.state.route;
1101
+ if (route?.info?.pluginName !== "admin" || route.handler !== ADMIN_LOGOUT_HANDLER) return;
1102
+ const id2 = ctx.state.user?.id;
1073
1103
  if (typeof id2 !== "number") return;
1074
1104
  try {
1075
1105
  await strapi2.plugin("micelio").service("preview").revoke(id2);
@@ -1092,6 +1122,9 @@ async function registerStatusPermission(strapi2) {
1092
1122
  }
1093
1123
  const bootstrap = async ({ strapi: strapi2 }) => {
1094
1124
  await ensureDefaultSite(strapi2);
1125
+ const backfilled = await backfillSite(strapi2);
1126
+ if (backfilled > 0)
1127
+ strapi2.log.info(`[micelio] gave the default site to ${backfilled} categories and tags`);
1095
1128
  await ensureSiteSections(strapi2);
1096
1129
  await strapi2.plugin("micelio").service("hostCheck").enforce();
1097
1130
  registerScheduler(strapi2);
@@ -1105,7 +1138,7 @@ const DEFAULT_CONFIG = {
1105
1138
  scheduledPublishRule: "* * * * *",
1106
1139
  statusRefreshRule: "0 4 * * *"
1107
1140
  };
1108
- const config = {
1141
+ const config$1 = {
1109
1142
  default: DEFAULT_CONFIG,
1110
1143
  validator(config2) {
1111
1144
  for (const key of ["scheduledPublishRule", "statusRefreshRule"]) {
@@ -1116,13 +1149,41 @@ const config = {
1116
1149
  }
1117
1150
  }
1118
1151
  };
1152
+ const kind$3 = "collectionType";
1153
+ const collectionName$a = "categories";
1154
+ const info$a = { "singularName": "category", "pluralName": "categories", "displayName": "Category", "description": "Organize your content into categories" };
1155
+ const options$a = { "draftAndPublish": false };
1156
+ const pluginOptions$3 = { "content-manager": { "visible": true }, "i18n": { "localized": true } };
1157
+ 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 } } } };
1158
+ const category = {
1159
+ kind: kind$3,
1160
+ collectionName: collectionName$a,
1161
+ info: info$a,
1162
+ options: options$a,
1163
+ pluginOptions: pluginOptions$3,
1164
+ attributes: attributes$a
1165
+ };
1166
+ const kind$2 = "collectionType";
1167
+ const collectionName$9 = "micelio_preview_credentials";
1168
+ 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)" };
1169
+ const options$9 = { "draftAndPublish": false };
1170
+ const pluginOptions$2 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false }, "i18n": { "localized": false }, "micelio": { "export": "never" } };
1171
+ 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" } };
1172
+ const previewCredential = {
1173
+ kind: kind$2,
1174
+ collectionName: collectionName$9,
1175
+ info: info$9,
1176
+ options: options$9,
1177
+ pluginOptions: pluginOptions$2,
1178
+ attributes: attributes$9
1179
+ };
1119
1180
  const kind$1 = "collectionType";
1120
- const collectionName$8 = "micelio_preview_credentials";
1121
- 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" };
1181
+ const collectionName$8 = "micelio_sites";
1182
+ 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" };
1122
1183
  const options$8 = { "draftAndPublish": false };
1123
- const pluginOptions$1 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false }, "i18n": { "localized": false }, "micelio": { "export": "never" } };
1124
- 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" } };
1125
- const previewCredential = {
1184
+ const pluginOptions$1 = { "content-manager": { "visible": true }, "content-type-builder": { "visible": false }, "i18n": { "localized": true } };
1185
+ 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 } } } };
1186
+ const site = {
1126
1187
  kind: kind$1,
1127
1188
  collectionName: collectionName$8,
1128
1189
  info: info$8,
@@ -1131,12 +1192,12 @@ const previewCredential = {
1131
1192
  attributes: attributes$8
1132
1193
  };
1133
1194
  const kind = "collectionType";
1134
- const collectionName$7 = "micelio_sites";
1135
- 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" };
1195
+ const collectionName$7 = "tags";
1196
+ const info$7 = { "singularName": "tag", "pluralName": "tags", "displayName": "Tag", "description": "Topics an article covers, used to filter the blog" };
1136
1197
  const options$7 = { "draftAndPublish": false };
1137
- const pluginOptions = { "content-manager": { "visible": true }, "content-type-builder": { "visible": false }, "i18n": { "localized": true } };
1138
- 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 } } } };
1139
- const site = {
1198
+ const pluginOptions = { "content-manager": { "visible": true }, "i18n": { "localized": true } };
1199
+ 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 } } } };
1200
+ const tag = {
1140
1201
  kind,
1141
1202
  collectionName: collectionName$7,
1142
1203
  info: info$7,
@@ -1146,8 +1207,13 @@ const site = {
1146
1207
  };
1147
1208
  const contentTypes = {
1148
1209
  site: { schema: site },
1210
+ category: { schema: withSiteScope(category) },
1211
+ tag: { schema: withSiteScope(tag) },
1149
1212
  "preview-credential": { schema: withSiteScope(previewCredential) }
1150
1213
  };
1214
+ const CATEGORY_UID = "plugin::micelio.category";
1215
+ const TAG_UID = "plugin::micelio.tag";
1216
+ const READ_ACTIONS$1 = [`${CATEGORY_UID}.find`, `${TAG_UID}.find`];
1151
1217
  const blocks = () => ({
1152
1218
  find(ctx) {
1153
1219
  ctx.body = {
@@ -1165,18 +1231,19 @@ const PREVIEW_PATH = "/_preview";
1165
1231
  const PREVIEW_GRANT_HEADER = "x-micelio-preview-grant";
1166
1232
  const PREVIEW_TOKEN_TTL_SECONDS = 60;
1167
1233
  const PREVIEW_GRANT_TTL_SECONDS = 15 * 60;
1234
+ const PREVIEW_SYSTEM_GRANT_TTL_SECONDS = 60;
1168
1235
  const PREVIEW_CREDENTIAL_RETENTION_SECONDS = 60 * 60;
1169
1236
  const PREVIEW_RATE_WINDOW_SECONDS = 60;
1170
1237
  const PREVIEW_TOKEN_RATE_LIMIT = 20;
1171
1238
  const PREVIEW_EXCHANGE_RATE_LIMIT = 60;
1172
1239
  async function readableDraft(strapi2, userAbility, contentType, documentId, locale) {
1173
1240
  const uid = contentType;
1174
- if (!strapi2.contentType(uid)?.attributes?.[SITE_ATTRIBUTE]) return { status: "not-site-scoped" };
1241
+ if (!strapi2.contentType(uid)?.attributes?.[SITE_ATTRIBUTE$1]) return { status: "not-site-scoped" };
1175
1242
  const entry = await strapi2.documents(uid).findOne({
1176
1243
  documentId,
1177
1244
  locale: locale ?? void 0,
1178
1245
  status: "draft",
1179
- populate: { [SITE_ATTRIBUTE]: { fields: ["documentId"] }, createdBy: true }
1246
+ populate: { [SITE_ATTRIBUTE$1]: { fields: ["documentId"] }, createdBy: true }
1180
1247
  });
1181
1248
  if (!entry?.site) return { status: "missing" };
1182
1249
  const checker = strapi2.plugin("content-manager").service("permission-checker").create({ userAbility, model: contentType });
@@ -1191,9 +1258,338 @@ async function abilityOf(strapi2, adminUser) {
1191
1258
  if (!user || user.isActive === false) return null;
1192
1259
  return strapi2.service("admin::permission").engine.generateUserAbility(user);
1193
1260
  }
1261
+ const FILE_UID = "plugin::upload.file";
1262
+ const QUERY_KEYS = /* @__PURE__ */ new Set(["populate", "site"]);
1263
+ const ALWAYS_FIELDS = /* @__PURE__ */ new Set(["id", "documentId", "createdAt", "updatedAt"]);
1264
+ const NOT_SCALAR = /* @__PURE__ */ new Set(["relation", "component", "dynamiczone", "media"]);
1265
+ const NO_POPULATE = { all: false, children: /* @__PURE__ */ new Map() };
1266
+ class QueryProblem extends Error {
1267
+ }
1268
+ function readDraftQuery(strapi2, query, schema) {
1269
+ try {
1270
+ for (const key of Object.keys(query)) {
1271
+ if (!QUERY_KEYS.has(key)) {
1272
+ throw new QueryProblem(
1273
+ `The draft read takes only documentId, locale and populate, not "${key}"`
1274
+ );
1275
+ }
1276
+ }
1277
+ return { populate: parsePopulate(strapi2, query.populate, schema, "document", "populate") };
1278
+ } catch (error) {
1279
+ if (error instanceof QueryProblem) return { problem: error.message };
1280
+ throw error;
1281
+ }
1282
+ }
1283
+ async function readDraft(strapi2, options2) {
1284
+ const schema = strapi2.contentType(options2.uid);
1285
+ const params = await strapi2.contentAPI.sanitize.query(
1286
+ { populate: draftPopulate(strapi2, options2.populate, schema) },
1287
+ schema,
1288
+ { auth: options2.auth }
1289
+ );
1290
+ const entry = await strapi2.documents(options2.uid).findOne({
1291
+ ...params,
1292
+ documentId: options2.documentId,
1293
+ locale: options2.locale,
1294
+ status: "draft"
1295
+ });
1296
+ if (!entry) return null;
1297
+ await publishedRelations(strapi2, entry, schema, options2.populate);
1298
+ await keepToSite(strapi2, entry, schema, options2.siteDocumentId);
1299
+ return entry;
1300
+ }
1301
+ function fail(message) {
1302
+ throw new QueryProblem(message);
1303
+ }
1304
+ function attributeOf(schema, name2) {
1305
+ return Object.prototype.hasOwnProperty.call(schema.attributes, name2) ? schema.attributes[name2] : void 0;
1306
+ }
1307
+ function expand(value, path2) {
1308
+ const found = { all: false, pairs: [] };
1309
+ const add2 = (part2) => {
1310
+ if (part2 === void 0 || part2 === null || part2 === false || part2 === "false") return;
1311
+ if (part2 === true || part2 === "true") {
1312
+ found.all = true;
1313
+ } else if (typeof part2 === "string") {
1314
+ for (const name2 of part2.split(",").map((piece) => piece.trim())) {
1315
+ if (name2 === "") continue;
1316
+ if (name2 === "*") found.all = true;
1317
+ else {
1318
+ const [head = "", ...rest] = name2.split(".");
1319
+ found.pairs.push([head, rest.length > 0 ? { populate: rest.join(".") } : true]);
1320
+ }
1321
+ }
1322
+ } else if (Array.isArray(part2)) {
1323
+ for (const item of part2) {
1324
+ if (typeof item !== "string" && (typeof item !== "object" || item === null)) {
1325
+ fail(`${path2} must be a name, a list of names or an object`);
1326
+ }
1327
+ add2(item);
1328
+ }
1329
+ } else if (typeof part2 === "object") {
1330
+ found.pairs.push(...Object.entries(part2));
1331
+ } else {
1332
+ fail(`${path2} must be a name, a list of names or an object`);
1333
+ }
1334
+ };
1335
+ add2(value);
1336
+ return found;
1337
+ }
1338
+ function parsePopulate(strapi2, value, schema, scope, path2) {
1339
+ const { all: all2, pairs } = expand(value, path2);
1340
+ if (all2 && pairs.length > 0) fail(`${path2} cannot mix "*" with names`);
1341
+ if (all2 && scope === "related") fail(`${path2} cannot be "*" under a relation`);
1342
+ const children = /* @__PURE__ */ new Map();
1343
+ for (const [name2, sub] of pairs) {
1344
+ const here = `${path2}[${name2}]`;
1345
+ const attribute = attributeOf(schema, name2);
1346
+ if (!attribute || attribute.private === true) fail(`${here} is not an attribute of the type`);
1347
+ if (scope === "related" && attribute.type !== "media") {
1348
+ fail(`${here}: under a relation only a media attribute may be populated`);
1349
+ }
1350
+ if (children.has(name2)) fail(`${here} is named twice`);
1351
+ const node = parseNode(strapi2, sub, attribute, here);
1352
+ if (node) children.set(name2, node);
1353
+ }
1354
+ return { all: all2, children };
1355
+ }
1356
+ function parseNode(strapi2, sub, attribute, path2) {
1357
+ if (sub === false || sub === "false") return void 0;
1358
+ const options2 = sub === true || sub === "true" || sub === "" ? {} : sub;
1359
+ if (typeof options2 !== "object" || options2 === null || Array.isArray(options2)) {
1360
+ fail(`${path2} must be true or an object`);
1361
+ }
1362
+ const given = options2;
1363
+ const only = (...keys) => {
1364
+ for (const key of Object.keys(given)) {
1365
+ if (!keys.includes(key)) fail(`${path2} does not take "${key}"`);
1366
+ }
1367
+ };
1368
+ switch (attribute.type) {
1369
+ case "media":
1370
+ only("fields");
1371
+ return { fields: fieldsOf(given.fields, strapi2.contentType(FILE_UID), path2) };
1372
+ case "relation": {
1373
+ only("fields", "populate");
1374
+ const target = relationTarget(strapi2, attribute, path2);
1375
+ const schema = strapi2.contentType(target);
1376
+ const node = { fields: fieldsOf(given.fields, schema, path2) };
1377
+ if (given.populate !== void 0) {
1378
+ node.populate = parsePopulate(
1379
+ strapi2,
1380
+ given.populate,
1381
+ schema,
1382
+ "related",
1383
+ `${path2}[populate]`
1384
+ );
1385
+ }
1386
+ return node;
1387
+ }
1388
+ case "component":
1389
+ only("fields", "populate");
1390
+ return componentNode(strapi2, given, attribute.component, path2);
1391
+ case "dynamiczone": {
1392
+ only("on");
1393
+ if (given.on === void 0) return {};
1394
+ if (typeof given.on !== "object" || given.on === null || Array.isArray(given.on)) {
1395
+ fail(`${path2}[on] must be an object`);
1396
+ }
1397
+ const on = /* @__PURE__ */ new Map();
1398
+ for (const [uid, fragment] of Object.entries(given.on)) {
1399
+ if (!attribute.components?.includes(uid))
1400
+ fail(`${path2}[on][${uid}] is not a component of the zone`);
1401
+ const node = parseNode(
1402
+ strapi2,
1403
+ fragment,
1404
+ { type: "component", component: uid },
1405
+ `${path2}[on][${uid}]`
1406
+ );
1407
+ if (node) on.set(uid, node);
1408
+ }
1409
+ return { on };
1410
+ }
1411
+ default:
1412
+ return fail(`${path2} is not a relation, a component, a dynamic zone or a media attribute`);
1413
+ }
1414
+ }
1415
+ function componentNode(strapi2, given, uid, path2) {
1416
+ const component2 = strapi2.components[uid];
1417
+ const node = { fields: fieldsOf(given.fields, component2, path2) };
1418
+ if (given.populate !== void 0) {
1419
+ node.populate = parsePopulate(
1420
+ strapi2,
1421
+ given.populate,
1422
+ component2,
1423
+ "document",
1424
+ `${path2}[populate]`
1425
+ );
1426
+ }
1427
+ return node;
1428
+ }
1429
+ function relationTarget(strapi2, attribute, path2) {
1430
+ const target = attribute.target;
1431
+ if (typeof target !== "string" || target.startsWith("admin::") || !(target in strapi2.contentTypes)) {
1432
+ return fail(`${path2} is a relation the draft read does not follow`);
1433
+ }
1434
+ return target;
1435
+ }
1436
+ function fieldsOf(value, schema, path2) {
1437
+ if (value === void 0) return void 0;
1438
+ const names = (Array.isArray(value) ? value : [value]).flatMap((item) => {
1439
+ if (typeof item !== "string") return fail(`${path2}[fields] must be names of fields`);
1440
+ return item.split(",").map((piece) => piece.trim());
1441
+ });
1442
+ for (const name2 of names) {
1443
+ const attribute = attributeOf(schema, name2);
1444
+ const known = ALWAYS_FIELDS.has(name2) || attribute !== void 0 && attribute.private !== true;
1445
+ if (!known || attribute !== void 0 && NOT_SCALAR.has(attribute.type)) {
1446
+ fail(`${path2}[fields]: "${name2}" is not a field of the type`);
1447
+ }
1448
+ }
1449
+ return names.filter((name2) => name2 !== "");
1450
+ }
1451
+ function draftPopulate(strapi2, spec, schema) {
1452
+ if (spec.all) return "*";
1453
+ const populate = {};
1454
+ for (const [name2, node] of spec.children) {
1455
+ const attribute = attributeOf(schema, name2);
1456
+ if (attribute?.type === "media") {
1457
+ populate[name2] = node.fields ? { fields: node.fields } : true;
1458
+ } else if (attribute?.type === "component") {
1459
+ populate[name2] = componentPopulate(strapi2, node, attribute.component);
1460
+ } else if (attribute?.type === "dynamiczone") {
1461
+ populate[name2] = node.on ? {
1462
+ on: Object.fromEntries(
1463
+ [...node.on].map(([uid, fragment]) => [
1464
+ uid,
1465
+ componentPopulate(strapi2, fragment, uid)
1466
+ ])
1467
+ )
1468
+ } : true;
1469
+ } else {
1470
+ populate[name2] = true;
1471
+ }
1472
+ }
1473
+ return populate;
1474
+ }
1475
+ function componentPopulate(strapi2, node, uid) {
1476
+ const options2 = {};
1477
+ if (node.fields) options2.fields = node.fields;
1478
+ if (node.populate) options2.populate = draftPopulate(strapi2, node.populate, strapi2.components[uid]);
1479
+ return Object.keys(options2).length > 0 ? options2 : true;
1480
+ }
1481
+ async function publishedRelations(strapi2, entry, schema, spec) {
1482
+ for (const [name2, attribute] of Object.entries(schema.attributes)) {
1483
+ const value = entry[name2];
1484
+ if (value === void 0 || value === null) continue;
1485
+ const node = spec.children.get(name2) ?? (spec.all ? {} : void 0);
1486
+ if (attribute.type === "relation") {
1487
+ if (typeof attribute.target !== "string") {
1488
+ delete entry[name2];
1489
+ continue;
1490
+ }
1491
+ const target = attribute.target;
1492
+ const published = [];
1493
+ for (const related of Array.isArray(value) ? value : [value]) {
1494
+ const read = await publishedOf(strapi2, target, related, node ?? {});
1495
+ if (read) published.push(read);
1496
+ }
1497
+ entry[name2] = Array.isArray(value) ? published : published[0] ?? null;
1498
+ } else if (attribute.type === "component") {
1499
+ const component2 = strapi2.components[attribute.component];
1500
+ for (const child of Array.isArray(value) ? value : [value]) {
1501
+ await publishedRelations(strapi2, child, component2, node?.populate ?? NO_POPULATE);
1502
+ }
1503
+ } else if (attribute.type === "dynamiczone") {
1504
+ for (const child of value) {
1505
+ const uid = child.__component;
1506
+ if (!(uid in strapi2.components)) continue;
1507
+ await publishedRelations(
1508
+ strapi2,
1509
+ child,
1510
+ strapi2.components[uid],
1511
+ node?.on?.get(uid)?.populate ?? NO_POPULATE
1512
+ );
1513
+ }
1514
+ }
1515
+ }
1516
+ }
1517
+ async function publishedOf(strapi2, target, related, node) {
1518
+ if (typeof related.documentId !== "string") return null;
1519
+ const media2 = {};
1520
+ for (const [name2, child] of node.populate?.children ?? []) {
1521
+ media2[name2] = child.fields ? { fields: child.fields } : true;
1522
+ }
1523
+ return await strapi2.documents(target).findOne({
1524
+ documentId: related.documentId,
1525
+ locale: typeof related.locale === "string" ? related.locale : void 0,
1526
+ status: "published",
1527
+ fields: node.fields,
1528
+ populate: Object.keys(media2).length > 0 ? media2 : void 0
1529
+ });
1530
+ }
1531
+ async function keepToSite(strapi2, entry, schema, siteDocumentId) {
1532
+ const found = /* @__PURE__ */ new Map();
1533
+ await walk(strapi2, entry, schema, async (target, entries) => {
1534
+ if (!isSiteScoped(strapi2.contentType(target))) return entries;
1535
+ const ids = found.get(target) ?? /* @__PURE__ */ new Set();
1536
+ for (const { documentId } of entries) if (typeof documentId === "string") ids.add(documentId);
1537
+ found.set(target, ids);
1538
+ return entries;
1539
+ });
1540
+ if (found.size === 0) return;
1541
+ const owned = /* @__PURE__ */ new Map();
1542
+ for (const [target, ids] of found) {
1543
+ const published = strapi2.contentType(target).options?.draftAndPublish === true;
1544
+ const rows = await strapi2.db.query(target).findMany({
1545
+ select: ["documentId"],
1546
+ where: {
1547
+ documentId: { $in: [...ids] },
1548
+ [SITE_ATTRIBUTE$1]: { documentId: siteDocumentId },
1549
+ ...published ? { publishedAt: { $notNull: true } } : {}
1550
+ }
1551
+ });
1552
+ owned.set(target, new Set(rows.map((row) => row.documentId)));
1553
+ }
1554
+ await walk(strapi2, entry, schema, async (target, entries) => {
1555
+ const ids = owned.get(target);
1556
+ if (!isSiteScoped(strapi2.contentType(target))) return entries;
1557
+ return entries.filter(
1558
+ ({ documentId }) => typeof documentId === "string" && ids?.has(documentId) === true
1559
+ );
1560
+ });
1561
+ }
1562
+ async function walk(strapi2, entry, schema, visit) {
1563
+ for (const [name2, attribute] of Object.entries(schema.attributes)) {
1564
+ const value = entry[name2];
1565
+ if (value === void 0 || value === null) continue;
1566
+ if (attribute.type === "relation") {
1567
+ if (!("target" in attribute) || typeof attribute.target !== "string") {
1568
+ delete entry[name2];
1569
+ continue;
1570
+ }
1571
+ const target = attribute.target;
1572
+ const many = Array.isArray(value);
1573
+ const kept = await visit(target, many ? value : [value]);
1574
+ for (const child of kept) await walk(strapi2, child, strapi2.contentType(target), visit);
1575
+ entry[name2] = many ? kept : kept[0] ?? null;
1576
+ } else if (attribute.type === "component") {
1577
+ const component2 = strapi2.components[attribute.component];
1578
+ for (const child of Array.isArray(value) ? value : [value]) {
1579
+ await walk(strapi2, child, component2, visit);
1580
+ }
1581
+ } else if (attribute.type === "dynamiczone") {
1582
+ for (const child of value) {
1583
+ const component2 = strapi2.components[child.__component];
1584
+ if (component2) await walk(strapi2, child, component2, visit);
1585
+ }
1586
+ }
1587
+ }
1588
+ }
1194
1589
  const LIFETIME_SECONDS = {
1195
1590
  token: PREVIEW_TOKEN_TTL_SECONDS,
1196
- grant: PREVIEW_GRANT_TTL_SECONDS
1591
+ grant: PREVIEW_GRANT_TTL_SECONDS,
1592
+ system: PREVIEW_SYSTEM_GRANT_TTL_SECONDS
1197
1593
  };
1198
1594
  function hashCredential(value) {
1199
1595
  return node_crypto.createHash("sha256").update(value).digest("hex");
@@ -1201,6 +1597,9 @@ function hashCredential(value) {
1201
1597
  function issueCredential(kind2, binding, now = /* @__PURE__ */ new Date()) {
1202
1598
  const value = node_crypto.randomBytes(32).toString("base64url");
1203
1599
  const record = {
1600
+ adminUser: null,
1601
+ adminOrigin: null,
1602
+ entryVersion: null,
1204
1603
  ...binding,
1205
1604
  kind: kind2,
1206
1605
  hash: hashCredential(value),
@@ -1229,15 +1628,22 @@ class PreviewUnavailableError extends Error {
1229
1628
  const preview$1 = ({ strapi: strapi2 }) => {
1230
1629
  const store2 = () => strapi2.db.query(PREVIEW_CREDENTIAL_UID);
1231
1630
  async function save(record) {
1232
- const site2 = await strapi2.db.query(SITE_UID).findOne({ where: { documentId: record.siteDocumentId }, select: ["id"] });
1631
+ const site2 = await strapi2.db.query(SITE_UID$1).findOne({ where: { documentId: record.siteDocumentId }, select: ["id"] });
1233
1632
  await store2().create({ data: { ...record, site: site2?.id } });
1234
1633
  }
1235
- async function find(kind2, value) {
1634
+ async function find(kinds, value) {
1236
1635
  const row = await store2().findOne({
1237
- where: { hash: hashCredential(value), kind: kind2 }
1636
+ where: { hash: hashCredential(value), kind: { $in: kinds } }
1238
1637
  });
1239
1638
  return row;
1240
1639
  }
1640
+ async function removeExpired() {
1641
+ await store2().deleteMany({
1642
+ where: {
1643
+ expiresAt: { $lt: new Date(Date.now() - PREVIEW_CREDENTIAL_RETENTION_SECONDS * 1e3) }
1644
+ }
1645
+ });
1646
+ }
1241
1647
  async function revoke(adminUser, entryDocumentId) {
1242
1648
  await store2().updateMany({
1243
1649
  where: {
@@ -1258,43 +1664,56 @@ const preview$1 = ({ strapi: strapi2 }) => {
1258
1664
  }
1259
1665
  return {
1260
1666
  async issueToken(binding) {
1261
- const site2 = await strapi2.db.query(SITE_UID).findOne({ where: { documentId: binding.siteDocumentId }, select: ["url"] });
1667
+ const site2 = await strapi2.db.query(SITE_UID$1).findOne({ where: { documentId: binding.siteDocumentId }, select: ["url"] });
1262
1668
  const url = previewUrlOf(site2?.url);
1263
1669
  if (!url) throw new PreviewUnavailableError(binding.siteDocumentId);
1264
- await store2().deleteMany({
1265
- where: {
1266
- expiresAt: { $lt: new Date(Date.now() - PREVIEW_CREDENTIAL_RETENTION_SECONDS * 1e3) }
1267
- }
1268
- });
1670
+ await removeExpired();
1269
1671
  const { value, record } = issueCredential("token", binding);
1270
1672
  await save(record);
1271
1673
  url.searchParams.set("token", value);
1272
1674
  return { url: url.toString(), expiresAt: record.expiresAt.toISOString() };
1273
1675
  },
1274
1676
  async exchange(token, siteDocumentId) {
1275
- const row = await find("token", token);
1677
+ const row = await find(["token"], token);
1276
1678
  if (!row || !mayUseCredential(toRecord(row), "token", { siteDocumentId })) return null;
1277
1679
  const claimed = await store2().updateMany({
1278
1680
  where: { id: row.id, usedAt: null, revokedAt: null },
1279
1681
  data: { usedAt: /* @__PURE__ */ new Date() }
1280
1682
  });
1281
1683
  if (claimed.count !== 1) return null;
1282
- if (!await stillReadable(row)) return null;
1283
- const { value, record } = issueCredential("grant", bindingOf(row));
1684
+ const binding = adminBindingOf(row);
1685
+ if (!binding || !await stillReadable(binding)) return null;
1686
+ const { value, record } = issueCredential("grant", binding);
1284
1687
  await save(record);
1285
1688
  return {
1286
1689
  grant: value,
1287
1690
  expiresAt: record.expiresAt.toISOString(),
1288
- adminOrigin: row.adminOrigin,
1691
+ adminOrigin: binding.adminOrigin,
1289
1692
  contentType: row.contentType,
1290
1693
  documentId: row.entryDocumentId,
1291
1694
  locale: row.entryLocale
1292
1695
  };
1293
1696
  },
1697
+ async issueSystemGrant(binding) {
1698
+ await removeExpired();
1699
+ const { value, record } = issueCredential("system", binding);
1700
+ await save(record);
1701
+ return value;
1702
+ },
1294
1703
  async resolveGrant(grant, expected) {
1295
- const row = await find("grant", grant);
1296
- if (!row || !mayUseCredential(toRecord(row), "grant", expected)) return null;
1297
- return await stillReadable(row) ? bindingOf(row) : null;
1704
+ const row = await find(["grant", "system"], grant);
1705
+ if (!row || !mayUseCredential(toRecord(row), row.kind, expected)) return null;
1706
+ if (row.kind === "system") {
1707
+ const claimed = await store2().updateMany({
1708
+ where: { id: row.id, usedAt: null, revokedAt: null },
1709
+ data: { usedAt: /* @__PURE__ */ new Date() }
1710
+ });
1711
+ if (claimed.count !== 1) return null;
1712
+ return { ...targetOf(row), actor: "system", entryVersion: row.entryVersion };
1713
+ }
1714
+ const binding = adminBindingOf(row);
1715
+ if (!binding || !await stillReadable(binding)) return null;
1716
+ return { ...targetOf(row), actor: "admin", entryVersion: null };
1298
1717
  },
1299
1718
  revoke
1300
1719
  };
@@ -1311,21 +1730,26 @@ function previewUrlOf(url) {
1311
1730
  return void 0;
1312
1731
  }
1313
1732
  }
1314
- function bindingOf(row) {
1733
+ function targetOf(row) {
1315
1734
  return {
1316
- adminUser: row.adminUser,
1317
- adminOrigin: row.adminOrigin,
1318
1735
  siteDocumentId: row.siteDocumentId,
1319
1736
  contentType: row.contentType,
1320
1737
  entryDocumentId: row.entryDocumentId,
1321
1738
  entryLocale: row.entryLocale
1322
1739
  };
1323
1740
  }
1741
+ function adminBindingOf(row) {
1742
+ if (row.adminUser === null || row.adminOrigin === null) return null;
1743
+ return { ...targetOf(row), adminUser: row.adminUser, adminOrigin: row.adminOrigin };
1744
+ }
1324
1745
  function toRecord(row) {
1325
1746
  return {
1326
- ...bindingOf(row),
1747
+ ...targetOf(row),
1327
1748
  kind: row.kind,
1328
1749
  hash: row.hash,
1750
+ adminUser: row.adminUser,
1751
+ adminOrigin: row.adminOrigin,
1752
+ entryVersion: row.entryVersion,
1329
1753
  expiresAt: new Date(row.expiresAt),
1330
1754
  usedAt: row.usedAt ? new Date(row.usedAt) : null,
1331
1755
  revokedAt: row.revokedAt ? new Date(row.revokedAt) : null
@@ -1363,6 +1787,16 @@ const preview = ({ strapi: strapi2 }) => {
1363
1787
  const previews = () => strapi2.plugin("micelio").service("preview");
1364
1788
  const tokenLimit = createRateLimiter(PREVIEW_TOKEN_RATE_LIMIT, PREVIEW_RATE_WINDOW_SECONDS);
1365
1789
  const exchangeLimit = createRateLimiter(PREVIEW_EXCHANGE_RATE_LIMIT, PREVIEW_RATE_WINDOW_SECONDS);
1790
+ async function isVersionOf(uid, binding) {
1791
+ const current = await strapi2.documents(uid).findOne({
1792
+ documentId: binding.entryDocumentId,
1793
+ locale: binding.entryLocale ?? void 0,
1794
+ status: "draft",
1795
+ fields: ["updatedAt"]
1796
+ });
1797
+ if (!current?.updatedAt || !binding.entryVersion) return false;
1798
+ return new Date(current.updatedAt).getTime() === new Date(binding.entryVersion).getTime();
1799
+ }
1366
1800
  return {
1367
1801
  async token(ctx) {
1368
1802
  const ability = ctx.state.userAbility;
@@ -1466,22 +1900,37 @@ const preview = ({ strapi: strapi2 }) => {
1466
1900
  }
1467
1901
  const uid = binding.contentType;
1468
1902
  const schema = strapi2.contentType(uid);
1903
+ const read = readDraftQuery(strapi2, query, schema);
1904
+ if ("problem" in read) {
1905
+ refuse(ctx, 400, "previewRequestInvalid", read.problem);
1906
+ return;
1907
+ }
1469
1908
  const auth = ctx.state.auth;
1470
- const params = await strapi2.contentAPI.sanitize.query(query, schema, { auth });
1471
- const entry = await strapi2.documents(uid).findOne({
1472
- ...params,
1473
- documentId: binding.entryDocumentId,
1474
- locale: binding.entryLocale ?? void 0,
1475
- status: "draft"
1476
- });
1477
- if (!entry) {
1909
+ let entry;
1910
+ try {
1911
+ entry = await readDraft(strapi2, {
1912
+ uid,
1913
+ documentId: binding.entryDocumentId,
1914
+ locale: binding.entryLocale ?? void 0,
1915
+ populate: read.populate,
1916
+ siteDocumentId: siteOf(ctx),
1917
+ auth
1918
+ });
1919
+ } catch (error) {
1920
+ if (!(error instanceof utils.errors.ValidationError || error instanceof utils.errors.PaginationError)) {
1921
+ throw error;
1922
+ }
1923
+ refuse(ctx, 400, "previewRequestInvalid", error.message);
1924
+ return;
1925
+ }
1926
+ if (!entry || binding.actor === "system" && !await isVersionOf(uid, binding)) {
1478
1927
  refuse(ctx, 404, "previewRefused", REFUSED);
1479
1928
  return;
1480
1929
  }
1481
1930
  ctx.set("Cache-Control", "private, no-store");
1482
1931
  ctx.body = {
1483
1932
  data: await strapi2.contentAPI.sanitize.output(entry, schema, { auth }),
1484
- meta: {}
1933
+ meta: { actor: binding.actor }
1485
1934
  };
1486
1935
  }
1487
1936
  };
@@ -1514,8 +1963,8 @@ function readTokenBody(body) {
1514
1963
  return { uid, documentId, locale };
1515
1964
  }
1516
1965
  const name = "@micelio/strapi-plugin-core";
1517
- const version$1 = "0.1.0";
1518
- const engines = { "node": ">=22.12" };
1966
+ const version$1 = "0.2.0";
1967
+ const engines = { "node": ">=22.19.0" };
1519
1968
  const micelio = "0.6";
1520
1969
  const packageJson = {
1521
1970
  name,
@@ -1760,7 +2209,7 @@ function runSanitiser(loaded, html) {
1760
2209
  return void 0;
1761
2210
  }
1762
2211
  async function readSiteOrigins(strapi2) {
1763
- const sites = await strapi2.db.query(SITE_UID).findMany({ select: ["url"] });
2212
+ const sites = await strapi2.db.query(SITE_UID$1).findMany({ select: ["url"] });
1764
2213
  const origins = /* @__PURE__ */ new Set();
1765
2214
  for (const { url } of sites) {
1766
2215
  if (!url) continue;
@@ -1818,7 +2267,7 @@ async function readSitesWithSections(strapi2, sections) {
1818
2267
  async function readSiteRows(strapi2, populate) {
1819
2268
  const locales = strapi2.plugin("i18n").service("locales");
1820
2269
  const defaultLocale = await locales.getDefaultLocale();
1821
- const rows = await strapi2.db.query(SITE_UID).findMany({
2270
+ const rows = await strapi2.db.query(SITE_UID$1).findMany({
1822
2271
  select: ["documentId", "locale", "name", "slug", "hosts", "isDefault", "url"],
1823
2272
  populate,
1824
2273
  orderBy: { slug: "asc" }
@@ -1881,7 +2330,7 @@ const extensions$1 = ({ strapi: strapi2 }) => {
1881
2330
  },
1882
2331
  async sites(ctx) {
1883
2332
  const ability = abilityOf2(ctx);
1884
- if (ability?.can(READ_SITES, SITE_UID) !== true) {
2333
+ if (ability?.can(READ_SITES, SITE_UID$1) !== true) {
1885
2334
  sendProblem(ctx, {
1886
2335
  type: "about:blank",
1887
2336
  title: "Forbidden",
@@ -1980,7 +2429,9 @@ function readBody(body) {
1980
2429
  return { uid, documentId, to, locale, publishAt };
1981
2430
  }
1982
2431
  const controllers = {
1983
- site: strapi.factories.createCoreController(SITE_UID),
2432
+ site: strapi.factories.createCoreController(SITE_UID$1),
2433
+ category: strapi.factories.createCoreController(CATEGORY_UID),
2434
+ tag: strapi.factories.createCoreController(TAG_UID),
1984
2435
  blocks,
1985
2436
  transitions: transitions$1,
1986
2437
  status: status$1,
@@ -2020,7 +2471,7 @@ const siteScope = (_config, { strapi: strapi2 }) => {
2020
2471
  };
2021
2472
  };
2022
2473
  async function loadSites(strapi2) {
2023
- const rows = await strapi2.db.query(SITE_UID).findMany({
2474
+ const rows = await strapi2.db.query(SITE_UID$1).findMany({
2024
2475
  select: ["documentId", "slug", "hosts", "isDefault"]
2025
2476
  });
2026
2477
  const sites = /* @__PURE__ */ new Map();
@@ -2122,16 +2573,16 @@ const COMPONENTS = {
2122
2573
  function registerComponents(strapi2) {
2123
2574
  const components = Object.fromEntries(
2124
2575
  Object.entries(COMPONENTS).flatMap(
2125
- ([category, schemas]) => Object.entries(schemas).map(([name2, schema]) => {
2126
- const uid = `${category}.${name2}`;
2576
+ ([category2, schemas]) => Object.entries(schemas).map(([name2, schema]) => {
2577
+ const uid = `${category2}.${name2}`;
2127
2578
  const component2 = {
2128
2579
  ...structuredClone(schema),
2129
2580
  __schema__: structuredClone(schema),
2130
2581
  uid,
2131
- category,
2582
+ category: category2,
2132
2583
  modelType: "component",
2133
2584
  modelName: name2,
2134
- globalId: `Component${pascalCase(category)}${pascalCase(name2)}`
2585
+ globalId: `Component${pascalCase(category2)}${pascalCase(name2)}`
2135
2586
  };
2136
2587
  return [uid, component2];
2137
2588
  })
@@ -2179,10 +2630,120 @@ async function storedState(strapi2, uid, params) {
2179
2630
  });
2180
2631
  return entry?.editorial?.state ?? "draft";
2181
2632
  }
2633
+ const LEGACY_TO_PLUGIN = {
2634
+ "api::category.category": "plugin::micelio.category",
2635
+ "api::tag.tag": "plugin::micelio.tag"
2636
+ };
2637
+ const CONFIGURATION_KEY_PREFIX = "plugin_content_manager_configuration_content_types::";
2638
+ const ACTION_TABLES = [
2639
+ { uid: "plugin::users-permissions.permission", owner: "role" },
2640
+ { uid: "admin::api-token-permission", owner: "token" }
2641
+ ];
2642
+ async function renameLegacyUids(strapi2) {
2643
+ const report = { actions: 0, subjects: 0, layouts: 0 };
2644
+ for (const [legacy, current] of Object.entries(LEGACY_TO_PLUGIN)) {
2645
+ if (strapi2.contentTypes[legacy]) {
2646
+ strapi2.log.info(
2647
+ `micelio: kept the grants and layouts of ${legacy}: the host still registers that type`
2648
+ );
2649
+ continue;
2650
+ }
2651
+ for (const { uid, owner } of ACTION_TABLES) {
2652
+ report.actions += await renameActions(strapi2, uid, owner, legacy, current);
2653
+ }
2654
+ report.subjects += await renameSubjects(strapi2, legacy, current);
2655
+ report.layouts += await renameLayouts(strapi2, legacy, current);
2656
+ }
2657
+ return report;
2658
+ }
2659
+ async function hasTable(strapi2, uid) {
2660
+ if (!strapi2.db.metadata.has(uid)) return false;
2661
+ const tableName = strapi2.db.metadata.get(uid).tableName;
2662
+ return Boolean(tableName) && await strapi2.db.getSchemaConnection().hasTable(tableName);
2663
+ }
2664
+ async function renameActions(strapi2, uid, owner, legacy, current) {
2665
+ if (!await hasTable(strapi2, uid)) return 0;
2666
+ const rows = await strapi2.db.query(uid).findMany({
2667
+ where: { action: { $startsWith: `${legacy}.` } },
2668
+ populate: { [owner]: { select: ["id"] } }
2669
+ });
2670
+ for (const row of rows) {
2671
+ const action = `${current}${row.action.slice(legacy.length)}`;
2672
+ const ownerRow = row[owner];
2673
+ const twin = ownerRow ? await strapi2.db.query(uid).findOne({ where: { action, [owner]: ownerRow.id } }) : null;
2674
+ if (twin) {
2675
+ await strapi2.db.query(uid).delete({ where: { id: row.id } });
2676
+ } else {
2677
+ await strapi2.db.query(uid).update({ where: { id: row.id }, data: { action } });
2678
+ }
2679
+ }
2680
+ return rows.length;
2681
+ }
2682
+ async function renameSubjects(strapi2, legacy, current) {
2683
+ const uid = "admin::permission";
2684
+ if (!await hasTable(strapi2, uid)) return 0;
2685
+ const rows = await strapi2.db.query(uid).findMany({
2686
+ where: { subject: legacy },
2687
+ populate: { role: { select: ["id"] } }
2688
+ });
2689
+ for (const row of rows) {
2690
+ const twin = row.role ? await strapi2.db.query(uid).findOne({ where: { action: row.action, subject: current, role: row.role.id } }) : null;
2691
+ if (twin) {
2692
+ await strapi2.db.query(uid).delete({ where: { id: row.id } });
2693
+ } else {
2694
+ await strapi2.db.query(uid).update({ where: { id: row.id }, data: { subject: current } });
2695
+ }
2696
+ }
2697
+ return rows.length;
2698
+ }
2699
+ function replaceWholeUid(text2, legacy, current) {
2700
+ const escaped = legacy.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2701
+ return text2.replace(new RegExp(`${escaped}(?![A-Za-z0-9_-])`, "g"), () => current);
2702
+ }
2703
+ async function renameLayouts(strapi2, legacy, current) {
2704
+ const uid = "strapi::core-store";
2705
+ if (!await hasTable(strapi2, uid)) return 0;
2706
+ const rows = await strapi2.db.query(uid).findMany({
2707
+ where: { key: { $startsWith: CONFIGURATION_KEY_PREFIX } }
2708
+ });
2709
+ let changed = 0;
2710
+ for (const row of rows) {
2711
+ const key = replaceWholeUid(row.key, legacy, current);
2712
+ const value = row.value ? replaceWholeUid(row.value, legacy, current) : row.value;
2713
+ if (key === row.key && value === row.value) continue;
2714
+ changed += 1;
2715
+ if (key !== row.key && await strapi2.db.query(uid).findOne({ where: { key } })) {
2716
+ await strapi2.db.query(uid).delete({ where: { id: row.id } });
2717
+ } else {
2718
+ await strapi2.db.query(uid).update({ where: { id: row.id }, data: { key, value } });
2719
+ }
2720
+ }
2721
+ return changed;
2722
+ }
2723
+ function previewPageUrl(strapi2, uid, { documentId, locale }) {
2724
+ const schema = strapi2.contentType(uid);
2725
+ const hasDynamicZone = Object.values(schema?.attributes ?? {}).some(
2726
+ ({ type }) => type === "dynamiczone"
2727
+ );
2728
+ if (!documentId || schema?.kind !== "collectionType") return void 0;
2729
+ if (!isSiteScoped(schema) || !hasDynamicZone) return void 0;
2730
+ const base = strapi2.config.get("admin.url", "/admin");
2731
+ const page = `${base.replace(/\/$/, "")}/plugins/micelio/preview/${encodeURIComponent(uid)}/${encodeURIComponent(documentId)}`;
2732
+ return locale ? `${page}?locale=${encodeURIComponent(locale)}` : page;
2733
+ }
2734
+ function registerPreviewShortcut(strapi2) {
2735
+ if (strapi2.config.get("admin.preview") !== void 0) return;
2736
+ strapi2.config.set("admin.preview", {
2737
+ enabled: true,
2738
+ config: {
2739
+ handler: (uid, params) => previewPageUrl(strapi2, uid, params)
2740
+ }
2741
+ });
2742
+ }
2182
2743
  function siteCreatedHook(strapi2) {
2183
2744
  return async (context, next) => {
2184
2745
  const result = await next();
2185
- if (context.uid !== SITE_UID || context.action !== "create") return result;
2746
+ if (context.uid !== SITE_UID$1 || context.action !== "create") return result;
2186
2747
  const site2 = result;
2187
2748
  await hooksOf(strapi2).runAfter("siteCreated", {
2188
2749
  site: site2.documentId,
@@ -2201,14 +2762,16 @@ function siteScopeDocuments(strapi2) {
2201
2762
  const site2 = currentSite(strapi2);
2202
2763
  if (context.action === "create") {
2203
2764
  params.data = params.data ?? {};
2204
- if (isMissing(params.data[SITE_ATTRIBUTE])) {
2205
- if (!site2) throw new utils.errors.ValidationError(`${SITE_ATTRIBUTE} is required`);
2206
- params.data[SITE_ATTRIBUTE] = site2.documentId;
2765
+ if (site2) params.data[SITE_ATTRIBUTE$1] = site2.documentId;
2766
+ else if (isMissing(params.data[SITE_ATTRIBUTE$1])) {
2767
+ throw new utils.errors.ValidationError(`${SITE_ATTRIBUTE$1} is required`);
2207
2768
  }
2208
2769
  }
2209
- if (context.action === "update" && params.data && SITE_ATTRIBUTE in params.data) {
2210
- if (isMissing(params.data[SITE_ATTRIBUTE])) {
2211
- throw new utils.errors.ValidationError(`${SITE_ATTRIBUTE} is required`);
2770
+ if (context.action === "update" && params.data && SITE_ATTRIBUTE$1 in params.data) {
2771
+ if (site2) delete params.data[SITE_ATTRIBUTE$1];
2772
+ else if (isUnchanged(params.data[SITE_ATTRIBUTE$1])) delete params.data[SITE_ATTRIBUTE$1];
2773
+ else if (isMissing(params.data[SITE_ATTRIBUTE$1])) {
2774
+ throw new utils.errors.ValidationError(`${SITE_ATTRIBUTE$1} is required`);
2212
2775
  }
2213
2776
  }
2214
2777
  if (site2 && READ_ACTIONS.has(context.action)) {
@@ -2220,21 +2783,119 @@ function siteScopeDocuments(strapi2) {
2220
2783
  });
2221
2784
  if (owned === 0) throw new utils.errors.NotFoundError("Not Found");
2222
2785
  }
2223
- return next();
2786
+ if (context.action === "update" && params.documentId) {
2787
+ await keepSiteOfLocales(strapi2, context.uid, params);
2788
+ }
2789
+ const result = await next();
2790
+ if (rebuildsRelations(context.action, params) && params.documentId) {
2791
+ await repairSiteOfLocales(strapi2, context.uid, params);
2792
+ }
2793
+ return result;
2224
2794
  };
2225
2795
  }
2796
+ async function keepSiteOfLocales(strapi2, uid, params) {
2797
+ if (!isLocalized(strapi2, uid)) return;
2798
+ const locale = typeof params.locale === "string" ? params.locale : await strapi2.plugin("i18n").service("locales").getDefaultLocale();
2799
+ const { documentId } = params;
2800
+ const rows = await strapi2.db.query(uid).findMany({
2801
+ where: { documentId },
2802
+ populate: { [SITE_ATTRIBUTE$1]: { select: ["id", "documentId"] } }
2803
+ });
2804
+ if (rows.length === 0) return;
2805
+ const named = params.data?.[SITE_ATTRIBUTE$1];
2806
+ if (rows.some((row) => row.locale === locale)) {
2807
+ if (named !== void 0) await refuseMoveAwayFromOtherLocales(strapi2, rows, locale, named);
2808
+ return;
2809
+ }
2810
+ const sites = new Map(
2811
+ rows.flatMap((row) => row.site ? [[row.site.documentId, row.site.id]] : [])
2812
+ );
2813
+ const data = params.data = params.data ?? {};
2814
+ if (named !== void 0) {
2815
+ if (sites.size !== 1) return;
2816
+ const [[shared]] = [...sites];
2817
+ for (const documentIdOfNamed of await namedSites(strapi2, named)) {
2818
+ if (documentIdOfNamed !== shared) {
2819
+ throw new utils.errors.ValidationError(`${SITE_ATTRIBUTE$1} must be the site of the other locales`);
2820
+ }
2821
+ }
2822
+ } else if (sites.size !== 1) {
2823
+ throw new utils.errors.ValidationError(`${SITE_ATTRIBUTE$1} is required`);
2824
+ }
2825
+ data[SITE_ATTRIBUTE$1] = { connect: [{ id: [...sites.values()][0] }] };
2826
+ }
2827
+ async function refuseMoveAwayFromOtherLocales(strapi2, rows, locale, named) {
2828
+ const target = new Set(await namedSites(strapi2, named));
2829
+ const own = rows.filter((row) => row.locale === locale).flatMap((row) => row.site?.documentId ?? []);
2830
+ if (target.size === 0 || [...target].every((site2) => own.includes(site2))) return;
2831
+ const staysElsewhere = rows.some(
2832
+ (row) => row.locale !== locale && row.site && !target.has(row.site.documentId)
2833
+ );
2834
+ if (staysElsewhere) {
2835
+ throw new utils.errors.ValidationError(
2836
+ `${SITE_ATTRIBUTE$1} cannot change while other locales of the document stay on another site`
2837
+ );
2838
+ }
2839
+ }
2840
+ async function namedSites(strapi2, value) {
2841
+ if (typeof value === "string") return [value];
2842
+ if (typeof value === "number") return [await documentIdOfSite(strapi2, value)];
2843
+ if (Array.isArray(value))
2844
+ return (await Promise.all(value.map((v) => namedSites(strapi2, v)))).flat();
2845
+ if (typeof value !== "object" || value === null) return [];
2846
+ const { documentId, id: id2, connect, set } = value;
2847
+ if (typeof documentId === "string") return [documentId];
2848
+ if (typeof id2 === "number") return [await documentIdOfSite(strapi2, id2)];
2849
+ return [...await namedSites(strapi2, connect), ...await namedSites(strapi2, set)];
2850
+ }
2851
+ async function documentIdOfSite(strapi2, id2) {
2852
+ const row = await strapi2.db.query(SITE_UID$1).findOne({ where: { id: id2 } });
2853
+ return row?.documentId ?? String(id2);
2854
+ }
2855
+ function isLocalized(strapi2, uid) {
2856
+ const model = strapi2.contentType(uid);
2857
+ const options2 = model.pluginOptions;
2858
+ return options2?.i18n?.localized === true;
2859
+ }
2860
+ function rebuildsRelations(action, params) {
2861
+ return action === "publish" || action === "discardDraft" || action === "update" && params.status === "published";
2862
+ }
2863
+ async function repairSiteOfLocales(strapi2, uid, params) {
2864
+ if (!isLocalized(strapi2, uid)) return;
2865
+ const query = strapi2.db.query(uid);
2866
+ const rows = await query.findMany({
2867
+ where: {
2868
+ documentId: params.documentId,
2869
+ ...typeof params.locale === "string" && params.locale !== "*" ? { locale: params.locale } : {}
2870
+ },
2871
+ populate: { [SITE_ATTRIBUTE$1]: { select: ["id"] } }
2872
+ });
2873
+ for (const row of rows.filter((candidate) => !candidate.site)) {
2874
+ const other = rows.find((candidate) => candidate.locale === row.locale && candidate.site);
2875
+ if (other?.site) {
2876
+ await query.update({ where: { id: row.id }, data: { [SITE_ATTRIBUTE$1]: other.site.id } });
2877
+ }
2878
+ }
2879
+ }
2226
2880
  function currentSite(strapi2) {
2227
2881
  const state = strapi2.requestContext.get()?.state;
2228
2882
  return state?.micelio?.site;
2229
2883
  }
2230
2884
  function siteFilter(site2) {
2231
- return { [SITE_ATTRIBUTE]: { documentId: { $eq: site2.documentId } } };
2885
+ return { [SITE_ATTRIBUTE$1]: { documentId: { $eq: site2.documentId } } };
2886
+ }
2887
+ function isUnchanged(value) {
2888
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2889
+ const { connect, disconnect, ...rest } = value;
2890
+ const isEmpty = (list2) => list2 === void 0 || Array.isArray(list2) && list2.length === 0;
2891
+ return Object.keys(rest).length === 0 && isEmpty(connect) && isEmpty(disconnect);
2232
2892
  }
2233
2893
  function isMissing(value) {
2234
2894
  if (value === void 0 || value === null || value === "") return true;
2235
2895
  if (Array.isArray(value)) return value.length === 0;
2236
2896
  if (typeof value === "object") {
2237
- const { connect, set } = value;
2897
+ const { connect, set, documentId, id: id2 } = value;
2898
+ if (documentId !== void 0 || id2 !== void 0) return !(documentId || id2);
2238
2899
  const list2 = set ?? connect;
2239
2900
  return Array.isArray(list2) ? list2.length === 0 : list2 === void 0;
2240
2901
  }
@@ -2255,7 +2916,7 @@ function normalizeHosts(value) {
2255
2916
  }
2256
2917
  return hosts;
2257
2918
  }
2258
- const SITE = SITE_UID;
2919
+ const SITE = SITE_UID$1;
2259
2920
  function siteRules(strapi2) {
2260
2921
  return async (context, next) => {
2261
2922
  if (context.uid !== SITE) return next();
@@ -2304,6 +2965,13 @@ async function hostsOfOtherSites(strapi2, documentId) {
2304
2965
  }
2305
2966
  const register = ({ strapi: strapi2 }) => {
2306
2967
  registerComponents(strapi2);
2968
+ registerPreviewShortcut(strapi2);
2969
+ strapi2.hook("strapi::content-types.beforeSync").register(async () => {
2970
+ const report = await renameLegacyUids(strapi2);
2971
+ if (report.actions + report.subjects + report.layouts > 0) {
2972
+ strapi2.log.info(`[micelio] moved legacy category and tag UIDs: ${JSON.stringify(report)}`);
2973
+ }
2974
+ });
2307
2975
  strapi2.documents.use(siteCreatedHook(strapi2));
2308
2976
  strapi2.documents.use(siteRules(strapi2));
2309
2977
  strapi2.documents.use(siteSectionInstances(strapi2));
@@ -2380,14 +3048,32 @@ const contentApiRoutes = () => ({
2380
3048
  }
2381
3049
  ]
2382
3050
  });
2383
- const siteRoutes = strapi.factories.createCoreRouter(SITE_UID, {
3051
+ const siteRoutes = strapi.factories.createCoreRouter(SITE_UID$1, {
2384
3052
  prefix: "/micelio",
2385
3053
  only: ["find", "findOne"]
2386
3054
  });
3055
+ const scoped = () => ({ middlewares: [SITE_SCOPE_MIDDLEWARE] });
3056
+ const config = () => ({
3057
+ find: scoped(),
3058
+ findOne: scoped(),
3059
+ create: scoped(),
3060
+ update: scoped(),
3061
+ delete: scoped()
3062
+ });
3063
+ const categoryRoutes = strapi.factories.createCoreRouter(CATEGORY_UID, {
3064
+ prefix: "",
3065
+ config: config()
3066
+ });
3067
+ const tagRoutes = strapi.factories.createCoreRouter(TAG_UID, {
3068
+ prefix: "",
3069
+ config: config()
3070
+ });
2387
3071
  const routes = {
2388
3072
  "content-api": contentApiRoutes,
2389
3073
  admin: adminRoutes,
2390
- site: siteRoutes
3074
+ site: siteRoutes,
3075
+ category: categoryRoutes,
3076
+ tag: tagRoutes
2391
3077
  };
2392
3078
  const extensions = ({ strapi: strapi2 }) => createExtensionRegistry(strapi2);
2393
3079
  const hooks = ({ strapi: strapi2 }) => createHookRegistry(strapi2.log);
@@ -2630,7 +3316,7 @@ function asList(value) {
2630
3316
  function exportedTypes(strapi2, personal) {
2631
3317
  const types = [];
2632
3318
  for (const [uid, schema] of Object.entries(strapi2.contentTypes)) {
2633
- if (uid !== SITE_UID && !isSiteScoped(schema)) continue;
3319
+ if (uid !== SITE_UID$1 && !isSiteScoped(schema)) continue;
2634
3320
  if (!isWritten(exportPolicyOf(schema), personal)) continue;
2635
3321
  types.push({
2636
3322
  uid,
@@ -2646,20 +3332,20 @@ async function siteRowsOf(strapi2, type, site2, context) {
2646
3332
  const query = strapi2.db.query(type.uid);
2647
3333
  const owned = await query.findMany({
2648
3334
  select: ["documentId"],
2649
- where: { [SITE_ATTRIBUTE]: { documentId: site2.documentId } }
3335
+ where: { [SITE_ATTRIBUTE$1]: { documentId: site2.documentId } }
2650
3336
  });
2651
3337
  const documentIds = [...new Set(owned.map((row) => row.documentId))];
2652
3338
  if (documentIds.length === 0) return [];
2653
3339
  return await query.findMany({
2654
3340
  where: { documentId: { $in: documentIds } },
2655
- populate: populateOf(type.attributes, context, /* @__PURE__ */ new Set([SITE_ATTRIBUTE]))
3341
+ populate: populateOf(type.attributes, context, /* @__PURE__ */ new Set([SITE_ATTRIBUTE$1]))
2656
3342
  });
2657
3343
  }
2658
3344
  function attributesOf(strapi2, uid) {
2659
3345
  return strapi2.contentType(uid).attributes;
2660
3346
  }
2661
3347
  function documentsOf(type, rows, context) {
2662
- const skip = new Set(type.uid === SITE_UID ? [] : [SITE_ATTRIBUTE]);
3348
+ const skip = new Set(type.uid === SITE_UID$1 ? [] : [SITE_ATTRIBUTE$1]);
2663
3349
  const documents = /* @__PURE__ */ new Map();
2664
3350
  for (const row of rows) {
2665
3351
  const version2 = {
@@ -2869,15 +3555,15 @@ async function exportSite(strapi2, request, output) {
2869
3555
  },
2870
3556
  personal
2871
3557
  };
2872
- const siteRows = await strapi2.db.query(SITE_UID).findMany({
3558
+ const siteRows = await strapi2.db.query(SITE_UID$1).findMany({
2873
3559
  where: { slug: request.site },
2874
- populate: populateOf(attributesOf(strapi2, SITE_UID), context)
3560
+ populate: populateOf(attributesOf(strapi2, SITE_UID$1), context)
2875
3561
  });
2876
3562
  const site2 = siteRows[0];
2877
3563
  if (!site2) throw new ExportError("exportSiteNotFound", `There is no site "${request.site}"`);
2878
3564
  const contents = [];
2879
3565
  for (const type of types) {
2880
- const rows = type.uid === SITE_UID ? siteRows : await siteRowsOf(strapi2, type, site2, context);
3566
+ const rows = type.uid === SITE_UID$1 ? siteRows : await siteRowsOf(strapi2, type, site2, context);
2881
3567
  contents.push({ type: type.name, documents: documentsOf(type, rows, context) });
2882
3568
  }
2883
3569
  const index2 = await mediaIndex(strapi2, media2);
@@ -3413,7 +4099,7 @@ function all(types, strapi2) {
3413
4099
  async function targetSite(strapi2, staged, request) {
3414
4100
  const archived = staged.contents.get("site").documents[0];
3415
4101
  const data = archived.versions[0].data;
3416
- const rows = await strapi2.db.query(SITE_UID).findMany({
4102
+ const rows = await strapi2.db.query(SITE_UID$1).findMany({
3417
4103
  select: ["documentId", "slug", "hosts", "isDefault"]
3418
4104
  });
3419
4105
  const sites = [...new Map(rows.map((row) => [row.documentId, row])).values()];
@@ -3707,7 +4393,7 @@ async function writeContent(strapi2, plan2, files, request, report) {
3707
4393
  const context = deserialiseContext(strapi2, plan2, files, written);
3708
4394
  const shaOf = new Map([...files.entries()].map(([sha256, id2]) => [id2, sha256]));
3709
4395
  const ordered = [...plan2.types].sort(
3710
- (a, b) => a.uid === SITE_UID ? -1 : b.uid === SITE_UID ? 1 : compare(a.name, b.name)
4396
+ (a, b) => a.uid === SITE_UID$1 ? -1 : b.uid === SITE_UID$1 ? 1 : compare(a.name, b.name)
3711
4397
  );
3712
4398
  const pending = [];
3713
4399
  for (const type of ordered) {
@@ -3723,7 +4409,7 @@ async function writeContent(strapi2, plan2, files, request, report) {
3723
4409
  }
3724
4410
  if (request.mode === "replace") {
3725
4411
  for (const type of ordered) {
3726
- if (type.uid === SITE_UID) continue;
4412
+ if (type.uid === SITE_UID$1) continue;
3727
4413
  const kept = new Set(plan2.staged.contents.get(type.name).documents.map((d) => d.documentId));
3728
4414
  const rows = await siteRowsOf(strapi2, type, plan2.site, serialiseContext(strapi2, plan2, shaOf));
3729
4415
  for (const documentId of new Set(rows.map((row) => row.documentId))) {
@@ -3735,9 +4421,9 @@ async function writeContent(strapi2, plan2, files, request, report) {
3735
4421
  }
3736
4422
  }
3737
4423
  async function writeDocument(strapi2, plan2, type, document, context, shaOf) {
3738
- const isSite = type.uid === SITE_UID;
4424
+ const isSite = type.uid === SITE_UID$1;
3739
4425
  const documentId = isSite ? plan2.site.documentId : document.documentId;
3740
- const skip = new Set(isSite ? [] : [SITE_ATTRIBUTE]);
4426
+ const skip = new Set(isSite ? [] : [SITE_ATTRIBUTE$1]);
3741
4427
  const query = strapi2.db.query(type.uid);
3742
4428
  const rows = await query.findMany({
3743
4429
  where: { documentId },
@@ -3760,7 +4446,7 @@ async function writeDocument(strapi2, plan2, type, document, context, shaOf) {
3760
4446
  pending ||= result.pending > 0;
3761
4447
  const data = { ...result.data };
3762
4448
  if (!exists) {
3763
- if (!isSite) data[SITE_ATTRIBUTE] = plan2.site.documentId;
4449
+ if (!isSite) data[SITE_ATTRIBUTE$1] = plan2.site.documentId;
3764
4450
  await documents.create({ locale, data: { ...data, documentId } });
3765
4451
  exists = true;
3766
4452
  } else {
@@ -3813,7 +4499,7 @@ async function writeDocument(strapi2, plan2, type, document, context, shaOf) {
3813
4499
  }
3814
4500
  function desiredData(type, version2, plan2, rows) {
3815
4501
  const data = knownData(type.attributes, version2.data);
3816
- if (type.uid === SITE_UID) {
4502
+ if (type.uid === SITE_UID$1) {
3817
4503
  data.isDefault = rows[0]?.isDefault === true;
3818
4504
  if (!plan2.site.isNew) data.slug = plan2.site.slug;
3819
4505
  }
@@ -3822,7 +4508,7 @@ function desiredData(type, version2, plan2, rows) {
3822
4508
  async function checkOwner(strapi2, type, documentId, site2) {
3823
4509
  const rows = await strapi2.db.query(type.uid).findMany({
3824
4510
  where: { documentId },
3825
- populate: { [SITE_ATTRIBUTE]: { select: ["documentId"] } }
4511
+ populate: { [SITE_ATTRIBUTE$1]: { select: ["documentId"] } }
3826
4512
  });
3827
4513
  if (rows.some((row) => row.site?.documentId && row.site.documentId !== site2)) {
3828
4514
  throw new ExportError(
@@ -3900,7 +4586,9 @@ const transitions = ({ strapi: strapi2 }) => ({
3900
4586
  }
3901
4587
  });
3902
4588
  const services = {
3903
- site: strapi.factories.createCoreService(SITE_UID),
4589
+ site: strapi.factories.createCoreService(SITE_UID$1),
4590
+ category: strapi.factories.createCoreService(CATEGORY_UID),
4591
+ tag: strapi.factories.createCoreService(TAG_UID),
3904
4592
  transitions,
3905
4593
  hostCheck,
3906
4594
  hooks,
@@ -3913,7 +4601,7 @@ const index = {
3913
4601
  register,
3914
4602
  bootstrap,
3915
4603
  destroy,
3916
- config,
4604
+ config: config$1,
3917
4605
  controllers,
3918
4606
  routes,
3919
4607
  services,
@@ -3921,10 +4609,13 @@ const index = {
3921
4609
  policies,
3922
4610
  middlewares
3923
4611
  };
4612
+ exports.CATEGORY_UID = CATEGORY_UID;
3924
4613
  exports.EDITORIAL_STATES = EDITORIAL_STATES;
3925
- exports.SITE_ATTRIBUTE = SITE_ATTRIBUTE;
4614
+ exports.READ_ACTIONS = READ_ACTIONS$1;
4615
+ exports.SITE_ATTRIBUTE = SITE_ATTRIBUTE$1;
3926
4616
  exports.SITE_SCOPE_MIDDLEWARE = SITE_SCOPE_MIDDLEWARE;
3927
- exports.SITE_UID = SITE_UID;
4617
+ exports.SITE_UID = SITE_UID$1;
4618
+ exports.TAG_UID = TAG_UID;
3928
4619
  exports.default = index;
3929
4620
  exports.formatHostCheckReport = formatHostCheckReport;
3930
4621
  exports.missingLocales = missingLocales;