@localazy/strapi-plugin 1.3.0 → 1.6.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 (69) hide show
  1. package/README.md +30 -0
  2. package/dist/_chunks/{FetchIdentity-BJGoZPpM.mjs → FetchIdentity-Bm7bc5bS.mjs} +36 -44
  3. package/dist/_chunks/{FetchIdentity-CtBRO-Cn.js → FetchIdentity-CcEjakiz.js} +25 -33
  4. package/dist/_chunks/{LanguagesSelector-DJxgCwgM.mjs → LanguagesSelector-B1rwN7-Y.mjs} +4 -3
  5. package/dist/_chunks/{LanguagesSelector-CNaKrS-m.js → LanguagesSelector-DejYCwBl.js} +4 -3
  6. package/dist/_chunks/{LocalazyApp-CJRpsTKr.mjs → LocalazyApp-C4kbsnQh.mjs} +148 -35
  7. package/dist/_chunks/{LocalazyApp-B1qCCEPe.js → LocalazyApp-jPO7Yr1S.js} +154 -41
  8. package/dist/_chunks/{LocalazyContentTransferSetup-CLAWn95U.mjs → LocalazyContentTransferSetup-C1wDD9hA.mjs} +58 -22
  9. package/dist/_chunks/{LocalazyContentTransferSetup-CCxiLHdG.js → LocalazyContentTransferSetup-DZh4ZdSi.js} +63 -27
  10. package/dist/_chunks/{LocalazyGlobalSettings-BJ5qfFfK.js → LocalazyGlobalSettings-D76it6rn.js} +57 -26
  11. package/dist/_chunks/{LocalazyGlobalSettings-Z-4Ykc_J.mjs → LocalazyGlobalSettings-DOW5rjLn.mjs} +56 -25
  12. package/dist/_chunks/{LocalazyPanel-CL_mzzfD.mjs → LocalazyPanel-C_Z4W49T.mjs} +12 -6
  13. package/dist/_chunks/{LocalazyPanel-DE3TtHOQ.js → LocalazyPanel-D2LQpqii.js} +14 -8
  14. package/dist/_chunks/{LocalazyStatusColumn-R5n6q_cx.mjs → LocalazyStatusColumn-C_ZCd_s7.mjs} +10 -4
  15. package/dist/_chunks/{LocalazyStatusColumn-BMLcklG-.js → LocalazyStatusColumn-Dy6N4_X6.js} +10 -4
  16. package/dist/_chunks/{RequireLocalazyAuth-WKKFdQlR.js → RequireLocalazyAuth-BVwF0bRC.js} +31 -4
  17. package/dist/_chunks/{RequireLocalazyAuth-Dzsu6UNS.mjs → RequireLocalazyAuth-DNhTZZ_6.mjs} +32 -5
  18. package/dist/_chunks/{entry-exclusion-service-DxqTlZFR.mjs → entry-exclusion-service-CF_yNCly.mjs} +1 -1
  19. package/dist/_chunks/{entry-exclusion-service-CggmAKxq.js → entry-exclusion-service-CH3ZoJQN.js} +1 -1
  20. package/dist/_chunks/{has-model-changed-FMGVcFHU.mjs → has-model-changed-CVJEuGxM.mjs} +2 -2
  21. package/dist/_chunks/{has-model-changed-BR53L6pA.js → has-model-changed-DdYbyv0v.js} +2 -2
  22. package/dist/_chunks/{i18n-QelOdIB2.mjs → i18n-76ox2llT.mjs} +9 -2
  23. package/dist/_chunks/{i18n-CAsbU4pi.js → i18n-C5ucROBh.js} +9 -2
  24. package/dist/_chunks/{index-BmxGKspv.js → index-CEGaWWh3.js} +33 -11
  25. package/dist/_chunks/{index-BK7lmZSN.js → index-CTkxoioB.js} +1 -0
  26. package/dist/_chunks/{index-toauQlIK.mjs → index-DP2ugzc_.mjs} +34 -12
  27. package/dist/_chunks/{index-CHjIMO9T.mjs → index-shryj0jT.mjs} +19 -18
  28. package/dist/_chunks/{strapi-api-base-CT9lknwW.js → strapi-api-base-CCUuu7EY.js} +1 -1
  29. package/dist/_chunks/{strapi-api-base-BflplVsH.mjs → strapi-api-base-IiMEtDoH.mjs} +1 -1
  30. package/dist/admin/index.js +1 -1
  31. package/dist/admin/index.mjs +1 -1
  32. package/dist/admin/src/components/LocalazyStatusColumn.d.ts +1 -1
  33. package/dist/admin/src/constants/permissions.d.ts +29 -0
  34. package/dist/admin/src/modules/@common/components/LanguagesSelector.d.ts +1 -0
  35. package/dist/admin/src/modules/activity-logs/locale/en.d.ts +4 -0
  36. package/dist/admin/src/modules/activity-logs/services/debug-bundle-service.d.ts +10 -0
  37. package/dist/admin/src/modules/login/locale/en.d.ts +1 -0
  38. package/dist/admin/src/modules/plugin-settings/components/Tree.d.ts +1 -0
  39. package/dist/admin/src/modules/plugin-settings/components/TreeItem.d.ts +1 -0
  40. package/dist/admin/src/modules/plugin-settings/components/WebhookSetup.d.ts +4 -1
  41. package/dist/admin/src/modules/plugin-settings/locale/en.d.ts +2 -0
  42. package/dist/admin/src/modules/plugin-settings/services/plugin-settings-service.d.ts +4 -0
  43. package/dist/server/index.js +665 -38
  44. package/dist/server/index.mjs +665 -39
  45. package/dist/server/src/bootstrap.d.ts +1 -1
  46. package/dist/server/src/constants/permissions.d.ts +38 -0
  47. package/dist/server/src/controllers/debug-bundle-controller.d.ts +7 -0
  48. package/dist/server/src/controllers/index.d.ts +6 -0
  49. package/dist/server/src/controllers/plugin-settings-controller.d.ts +1 -0
  50. package/dist/server/src/core/index.d.ts +5 -1
  51. package/dist/server/src/db/model/activity-logs.d.ts +10 -0
  52. package/dist/server/src/index.d.ts +64 -8
  53. package/dist/server/src/routes/activity-logs-routes.d.ts +20 -3
  54. package/dist/server/src/routes/debug-bundle-routes.d.ts +14 -0
  55. package/dist/server/src/routes/entry-exclusion-routes.d.ts +20 -3
  56. package/dist/server/src/routes/index.d.ts +52 -6
  57. package/dist/server/src/routes/localazy-auth-routes.d.ts +6 -1
  58. package/dist/server/src/routes/localazy-project-routes.d.ts +20 -3
  59. package/dist/server/src/routes/localazy-transfer-routes.d.ts +6 -1
  60. package/dist/server/src/routes/localazy-user-routes.d.ts +20 -3
  61. package/dist/server/src/routes/plugin-settings-routes.d.ts +20 -3
  62. package/dist/server/src/routes/strapi-routes.d.ts +20 -3
  63. package/dist/server/src/services/activity-logs-service.d.ts +3 -1
  64. package/dist/server/src/services/index.d.ts +5 -1
  65. package/dist/server/src/services/localazy-transfer-download-service.d.ts +2 -1
  66. package/dist/server/src/services/plugin-settings-service.d.ts +4 -0
  67. package/dist/server/src/utils/build-debug-bundle.d.ts +68 -0
  68. package/dist/server/src/utils/sanitize-debug-bundle.d.ts +25 -0
  69. package/package.json +2 -1
@@ -1,5 +1,5 @@
1
1
  import require$$0$4 from "http";
2
- import require$$1$4 from "fs";
2
+ import require$$1$4, { promises } from "fs";
3
3
  import require$$1$1 from "zlib";
4
4
  import require$$1 from "path";
5
5
  import require$$4 from "stream";
@@ -9,6 +9,7 @@ import require$$0$2 from "crypto";
9
9
  import require$$0$3 from "events";
10
10
  import require$$2 from "timers";
11
11
  import require$$8, { WebSocketServer } from "ws";
12
+ import JSZip from "jszip";
12
13
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
13
14
  function getDefaultExportFromCjs(x2) {
14
15
  return x2 && x2.__esModule && Object.prototype.hasOwnProperty.call(x2, "default") ? x2["default"] : x2;
@@ -20519,7 +20520,7 @@ var distExports = dist$1.exports;
20519
20520
  const io = /* @__PURE__ */ getDefaultExportFromCjs(distExports);
20520
20521
  const { Server: Server2, Namespace, Socket: Socket3 } = io;
20521
20522
  const name = "@localazy/strapi-plugin";
20522
- const version = "1.3.0";
20523
+ const version = "1.6.0";
20523
20524
  const description = "The official Strapi Plugin by Localazy.";
20524
20525
  const keywords = [
20525
20526
  "strapi",
@@ -20589,6 +20590,7 @@ const scripts = {
20589
20590
  prepare: "husky || true"
20590
20591
  };
20591
20592
  const dependencies = {
20593
+ jszip: "^3.10.1",
20592
20594
  shelljs: "^0.8.5",
20593
20595
  ws: "^8.18.0"
20594
20596
  };
@@ -20702,7 +20704,60 @@ const _package$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePr
20702
20704
  version
20703
20705
  }, Symbol.toStringTag, { value: "Module" }));
20704
20706
  const PLUGIN_NAME = strapi$1.name;
20705
- const bootstrap = ({ strapi: strapi2 }) => {
20707
+ const PERMISSION_UIDS = {
20708
+ READ: `plugin::${PLUGIN_NAME}.read`,
20709
+ TRANSFER: `plugin::${PLUGIN_NAME}.transfer`,
20710
+ SETTINGS_UPDATE: `plugin::${PLUGIN_NAME}.settings.update`
20711
+ };
20712
+ const LOCALAZY_RBAC_ACTIONS = [
20713
+ {
20714
+ section: "plugins",
20715
+ subCategory: "General",
20716
+ pluginName: PLUGIN_NAME,
20717
+ displayName: "Read",
20718
+ uid: "read"
20719
+ },
20720
+ {
20721
+ section: "plugins",
20722
+ subCategory: "General",
20723
+ pluginName: PLUGIN_NAME,
20724
+ displayName: "Transfer",
20725
+ uid: "transfer"
20726
+ },
20727
+ {
20728
+ section: "plugins",
20729
+ subCategory: "Settings",
20730
+ pluginName: PLUGIN_NAME,
20731
+ displayName: "Read settings",
20732
+ uid: "settings.read"
20733
+ },
20734
+ {
20735
+ section: "plugins",
20736
+ subCategory: "Settings",
20737
+ pluginName: PLUGIN_NAME,
20738
+ displayName: "Update settings",
20739
+ uid: "settings.update"
20740
+ }
20741
+ ];
20742
+ const registerRbacActions = async (strapi2) => {
20743
+ try {
20744
+ const permissionService = strapi2.service("admin::permission");
20745
+ const actionProvider = permissionService?.actionProvider;
20746
+ if (!actionProvider?.registerMany) {
20747
+ strapi2.log.warn(
20748
+ `[${PLUGIN_NAME}] admin::permission.actionProvider unavailable — skipping RBAC action registration`
20749
+ );
20750
+ return;
20751
+ }
20752
+ await actionProvider.registerMany(LOCALAZY_RBAC_ACTIONS);
20753
+ } catch (error) {
20754
+ strapi2.log.error(
20755
+ `[${PLUGIN_NAME}] Failed to register RBAC actions — plugin permissions will not appear in role editor: ${error instanceof Error ? error.message : String(error)}`
20756
+ );
20757
+ }
20758
+ };
20759
+ const bootstrap = async ({ strapi: strapi2 }) => {
20760
+ await registerRbacActions(strapi2);
20706
20761
  if (strapi2.plugin(PLUGIN_NAME)?.config("enableSocketIO", true)) {
20707
20762
  process.nextTick(async () => {
20708
20763
  const io2 = new Server2(strapi2.server.httpServer, {
@@ -24285,6 +24340,9 @@ const PluginSettingsController = ({ strapi: _strapi }) => ({
24285
24340
  async updatePluginSettings(ctx) {
24286
24341
  ctx.body = await getPluginSettingsService().updatePluginSettings(ctx.request.body);
24287
24342
  },
24343
+ async updatePluginSettingsUiPrefs(ctx) {
24344
+ ctx.body = await getPluginSettingsService().updatePluginSettingsUiPrefs(ctx.request.body);
24345
+ },
24288
24346
  async getSyncCursor(ctx) {
24289
24347
  ctx.body = await getPluginSettingsService().getSyncCursor();
24290
24348
  }
@@ -24384,6 +24442,8 @@ const LocalazyTransferController = ({ strapi: strapi2 }) => ({
24384
24442
  try {
24385
24443
  const LocalazyTransferDownloadService2 = getLocalazyTransferDownloadService();
24386
24444
  let fullSync = ctx.request.body?.fullSync === true;
24445
+ const rawUiLanguages = ctx.request.body?.uiLanguages;
24446
+ const requestedUiLanguages = Array.isArray(rawUiLanguages) && rawUiLanguages.every((code) => typeof code === "string") ? rawUiLanguages : void 0;
24387
24447
  const requestInitiatorHelper = new RequestInitiatorHelper(strapi2);
24388
24448
  if (requestInitiatorHelper.isInitiatedByLocalazyWebhook()) {
24389
24449
  const pluginSettingsServiceHelper = new PluginSettingsServiceHelper();
@@ -24393,7 +24453,8 @@ const LocalazyTransferController = ({ strapi: strapi2 }) => ({
24393
24453
  setTimeout(
24394
24454
  () => LocalazyTransferDownloadService2.download({
24395
24455
  notificationService: JobNotificationService$1,
24396
- fullSync
24456
+ fullSync,
24457
+ requestedUiLanguages
24397
24458
  }),
24398
24459
  1e3
24399
24460
  );
@@ -24488,6 +24549,413 @@ const ActivityLogsController = ({ strapi: _strapi }) => ({
24488
24549
  ctx.body = { success: true };
24489
24550
  }
24490
24551
  });
24552
+ const URL_SECRET_KEYS = ["token", "secret", "signature", "key", "apikey", "api_key"];
24553
+ const DEBUG_BUNDLE_ENV_ALLOWLIST = [
24554
+ "STRAPI_ADMIN_LOCALAZY_ENV",
24555
+ "STRAPI_ADMIN_BACKEND_URL",
24556
+ "NODE_ENV",
24557
+ "STRAPI_TELEMETRY_DISABLED"
24558
+ ];
24559
+ const redactIdentity = (identity2) => {
24560
+ if (!identity2) return null;
24561
+ return {
24562
+ userId: identity2.user?.id ?? "",
24563
+ name: identity2.user?.name ?? "",
24564
+ scope: identity2.scope ?? "",
24565
+ hasAccessToken: Boolean(identity2.accessToken)
24566
+ };
24567
+ };
24568
+ const scrubUrlSecrets = (input) => {
24569
+ if (typeof input !== "string") return input;
24570
+ if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(input) && !input.includes("?")) return input;
24571
+ try {
24572
+ const url2 = new URL(input);
24573
+ let mutated = false;
24574
+ for (const k2 of [...url2.searchParams.keys()]) {
24575
+ if (URL_SECRET_KEYS.includes(k2.toLowerCase())) {
24576
+ url2.searchParams.delete(k2);
24577
+ mutated = true;
24578
+ }
24579
+ }
24580
+ return mutated ? url2.toString() : input;
24581
+ } catch {
24582
+ return input;
24583
+ }
24584
+ };
24585
+ const pickEnvAllowlist = (env) => {
24586
+ const out = {};
24587
+ for (const key of DEBUG_BUNDLE_ENV_ALLOWLIST) {
24588
+ const raw = env[key];
24589
+ const present = raw !== void 0;
24590
+ out[key] = {
24591
+ present,
24592
+ value: present ? raw : null
24593
+ };
24594
+ }
24595
+ return out;
24596
+ };
24597
+ const serializeContentTypeSchema = (contentType) => {
24598
+ if (!contentType || typeof contentType !== "object") return null;
24599
+ const ct = contentType;
24600
+ return {
24601
+ kind: ct.kind,
24602
+ collectionName: ct.collectionName,
24603
+ info: ct.info,
24604
+ options: ct.options,
24605
+ pluginOptions: ct.pluginOptions,
24606
+ attributes: ct.attributes
24607
+ };
24608
+ };
24609
+ const DOCUMENT_INTERNAL_FIELDS = ["createdBy", "updatedBy"];
24610
+ const stripDocumentInternals = (doc) => {
24611
+ if (!doc || typeof doc !== "object") return doc;
24612
+ const clone = { ...doc };
24613
+ for (const field of DOCUMENT_INTERNAL_FIELDS) {
24614
+ delete clone[field];
24615
+ }
24616
+ return clone;
24617
+ };
24618
+ class SessionNotFoundError extends Error {
24619
+ constructor(sessionId) {
24620
+ super(`Session ${sessionId} not found`);
24621
+ this.name = "SessionNotFoundError";
24622
+ }
24623
+ }
24624
+ const DEBUG_BUNDLE_SCHEMA_VERSION = 1;
24625
+ const CAPS = {
24626
+ perEntryBytes: 1 * 1024 * 1024,
24627
+ // 1 MB
24628
+ totalEntries: 100,
24629
+ errorLogBytes: 2 * 1024 * 1024,
24630
+ // 2 MB
24631
+ consoleErrorsLines: 200,
24632
+ browserPayloadBytes: 64 * 1024
24633
+ // 64 KB
24634
+ };
24635
+ const selectAttemptedEntries = (attempted) => {
24636
+ const failedByKey = /* @__PURE__ */ new Map();
24637
+ const successByKey = /* @__PURE__ */ new Map();
24638
+ for (const record of attempted) {
24639
+ const key = `${record.uid}|${record.documentId}|${record.locale}`;
24640
+ const selected = {
24641
+ uid: record.uid,
24642
+ documentId: record.documentId,
24643
+ locale: record.locale,
24644
+ status: record.status,
24645
+ attemptedAt: record.attemptedAt
24646
+ };
24647
+ if (record.status === "failed") {
24648
+ successByKey.delete(key);
24649
+ if (!failedByKey.has(key)) failedByKey.set(key, selected);
24650
+ } else if (!failedByKey.has(key) && !successByKey.has(key)) {
24651
+ successByKey.set(key, selected);
24652
+ }
24653
+ }
24654
+ return [...failedByKey.values(), ...successByKey.values()];
24655
+ };
24656
+ const formatTimestampForFilename = (ms2) => new Date(ms2).toISOString().replace(/[:.]/g, "-");
24657
+ const safeUidForPath = (uid) => uid.replace(/::/g, "--").replace(/\./g, "-");
24658
+ const decodeBrowserPayload = (encoded) => {
24659
+ const markers = [];
24660
+ if (!encoded) return { payload: null, truncationMarkers: markers };
24661
+ try {
24662
+ const raw = Buffer.from(encoded, "base64").toString("utf8");
24663
+ const rawBytes = Buffer.byteLength(raw, "utf8");
24664
+ if (rawBytes > CAPS.browserPayloadBytes) {
24665
+ markers.push({ kind: "browser-payload-truncated", originalSizeBytes: rawBytes });
24666
+ return { payload: null, truncationMarkers: markers };
24667
+ }
24668
+ const parsed = JSON.parse(raw);
24669
+ if (parsed && Array.isArray(parsed.consoleErrors) && parsed.consoleErrors.length > CAPS.consoleErrorsLines) {
24670
+ markers.push({ kind: "console-errors-truncated", originalCount: parsed.consoleErrors.length });
24671
+ parsed.consoleErrors = parsed.consoleErrors.slice(0, CAPS.consoleErrorsLines);
24672
+ }
24673
+ return { payload: parsed, truncationMarkers: markers };
24674
+ } catch {
24675
+ return { payload: null, truncationMarkers: markers };
24676
+ }
24677
+ };
24678
+ const deepScrubUrls = (value) => {
24679
+ if (value == null) return value;
24680
+ if (typeof value === "string") return scrubUrlSecrets(value);
24681
+ if (Array.isArray(value)) return value.map(deepScrubUrls);
24682
+ if (typeof value === "object") {
24683
+ const out = {};
24684
+ for (const [k2, v2] of Object.entries(value)) {
24685
+ out[k2] = deepScrubUrls(v2);
24686
+ }
24687
+ return out;
24688
+ }
24689
+ return value;
24690
+ };
24691
+ const sanitizePluginSettings = (settings) => {
24692
+ const cloned = JSON.parse(JSON.stringify(settings ?? {}));
24693
+ if (cloned.webhookConfig?.url) {
24694
+ cloned.webhookConfig.url = scrubUrlSecrets(cloned.webhookConfig.url);
24695
+ }
24696
+ return cloned;
24697
+ };
24698
+ const sanitizeSession = (session) => {
24699
+ const cloned = JSON.parse(JSON.stringify(session));
24700
+ for (const entry of cloned.entries ?? []) {
24701
+ if (entry.data !== void 0) {
24702
+ entry.data = deepScrubUrls(entry.data);
24703
+ }
24704
+ }
24705
+ return cloned;
24706
+ };
24707
+ const buildErrorLog = (session) => {
24708
+ const lines = [];
24709
+ for (const entry of session.entries ?? []) {
24710
+ const ts = Number.isFinite(entry.timestamp) ? new Date(entry.timestamp).toISOString() : "unknown";
24711
+ lines.push(`${ts} ${entry.message}`);
24712
+ }
24713
+ if (session.status === "failed" && session.summary) {
24714
+ lines.push(`ERROR: ${session.summary}`);
24715
+ }
24716
+ const full = lines.join("\n");
24717
+ const fullBytes = Buffer.byteLength(full, "utf8");
24718
+ if (fullBytes <= CAPS.errorLogBytes) {
24719
+ return { text: full, markers: [] };
24720
+ }
24721
+ const truncated = Buffer.from(full, "utf8").subarray(fullBytes - CAPS.errorLogBytes).toString("utf8");
24722
+ return {
24723
+ text: `...[truncated, kept last ${CAPS.errorLogBytes} bytes]
24724
+ ${truncated}`,
24725
+ markers: [{ kind: "error-log-truncated", originalSizeBytes: fullBytes }]
24726
+ };
24727
+ };
24728
+ const readHostPackages = async (strapi2) => {
24729
+ const appRoot = strapi2?.dirs?.app?.root ?? process.cwd();
24730
+ if (!appRoot) return { data: null, reason: "host-app-root-unknown" };
24731
+ try {
24732
+ const raw = await promises.readFile(require$$1.join(appRoot, "package.json"), "utf8");
24733
+ const parsed = JSON.parse(raw);
24734
+ return {
24735
+ data: {
24736
+ name: parsed.name,
24737
+ version: parsed.version,
24738
+ dependencies: parsed.dependencies ?? {},
24739
+ devDependencies: parsed.devDependencies ?? {}
24740
+ }
24741
+ };
24742
+ } catch (err) {
24743
+ return { data: null, reason: err instanceof Error ? err.message : String(err) };
24744
+ }
24745
+ };
24746
+ const buildReadme = ({
24747
+ sessionShortId,
24748
+ sessionId,
24749
+ schemaVersion,
24750
+ hostPackagesSkipped
24751
+ }) => {
24752
+ const lines = [];
24753
+ lines.push(`# Localazy Strapi Plugin — Troubleshooting Bundle`);
24754
+ lines.push("");
24755
+ lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
24756
+ lines.push(`Session id: ${sessionId}`);
24757
+ lines.push(`Schema version: ${schemaVersion}`);
24758
+ lines.push("");
24759
+ lines.push("## Contents");
24760
+ lines.push("");
24761
+ lines.push("- `meta.json` — schema version, session metadata.");
24762
+ lines.push("- `versions.json` — plugin version, Strapi version, Node version, platform/arch.");
24763
+ lines.push("- `env.json` — allow-listed environment variables only.");
24764
+ lines.push("- `browser.json` — user agent, language, viewport, console errors (from the admin UI).");
24765
+ lines.push("- `session.json` — full activity-log session record (URLs scrubbed of secret query keys).");
24766
+ lines.push("- `error.log` — chronological text log of session entries.");
24767
+ lines.push("- `plugin-settings.json` — full plugin settings (webhook URL secrets scrubbed).");
24768
+ lines.push("- `content-transfer-setup.json` — content-transfer setup record.");
24769
+ if (hostPackagesSkipped) {
24770
+ lines.push("- `host-packages.json` — NOT INCLUDED (could not be read; see `bundle-truncated.json`).");
24771
+ } else {
24772
+ lines.push("- `host-packages.json` — host project's package.json (deps + devDeps only).");
24773
+ }
24774
+ lines.push("- `content-types/` — schema.json per touched content-type UID.");
24775
+ lines.push("- `entries/<uid>/<documentId>--<locale>.json` — per-entry payload from `strapi.documents().findOne()`.");
24776
+ lines.push("- `bundle-truncated.json` — present only if any per-section cap was exceeded.");
24777
+ lines.push("");
24778
+ lines.push("## Reproducing locally");
24779
+ lines.push("");
24780
+ lines.push("1. `unzip` the bundle.");
24781
+ lines.push("2. Recreate the content types from `content-types/*.schema.json` in your Strapi project.");
24782
+ lines.push("3. Import the entries via the Document Service using `entries/<uid>/*.json`.");
24783
+ lines.push("4. Apply `plugin-settings.json` and `content-transfer-setup.json` via the plugin admin UI.");
24784
+ lines.push("5. Trigger the same upload/download/webhook with the same locale set.");
24785
+ lines.push("6. Compare the failure against `error.log` and `session.json`.");
24786
+ lines.push("");
24787
+ lines.push(`Bundle short id: ${sessionShortId}`);
24788
+ return lines.join("\n");
24789
+ };
24790
+ const buildDebugBundle = async ({
24791
+ strapi: strapi2,
24792
+ sessionId,
24793
+ browserPayloadEncoded
24794
+ }) => {
24795
+ const zip = new JSZip();
24796
+ const truncationMarkers = [];
24797
+ const pluginName = "localazy";
24798
+ const activityLogsService = strapi2.plugin(pluginName).service("ActivityLogsService");
24799
+ const pluginSettingsService = strapi2.plugin(pluginName).service("PluginSettingsService");
24800
+ const strapiService = strapi2.plugin(pluginName).service("StrapiService");
24801
+ const localazyUserService = strapi2.plugin(pluginName).service("LocalazyUserService");
24802
+ const session = await activityLogsService.getSession(sessionId);
24803
+ if (!session) {
24804
+ throw new SessionNotFoundError(sessionId);
24805
+ }
24806
+ const sessionShortId = session.id.slice(0, 8);
24807
+ const pluginSettingsRaw = await pluginSettingsService.getPluginSettings();
24808
+ const contentTransferSetup = await pluginSettingsService.getContentTransferSetup();
24809
+ const identity2 = await localazyUserService.getUser();
24810
+ const redactedIdentity = redactIdentity(identity2);
24811
+ const pluginVersion = await strapiService.getPluginVersion();
24812
+ const strapiVersion = strapiService.getStrapiVersion();
24813
+ zip.file(
24814
+ "meta.json",
24815
+ JSON.stringify(
24816
+ {
24817
+ schemaVersion: DEBUG_BUNDLE_SCHEMA_VERSION,
24818
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
24819
+ sessionId: session.id,
24820
+ sessionShortId,
24821
+ eventType: session.eventType,
24822
+ status: session.status,
24823
+ startedAt: session.startedAt,
24824
+ finishedAt: session.finishedAt ?? null,
24825
+ initiatedBy: session.initiatedBy,
24826
+ identity: redactedIdentity
24827
+ },
24828
+ null,
24829
+ 2
24830
+ )
24831
+ );
24832
+ zip.file(
24833
+ "versions.json",
24834
+ JSON.stringify(
24835
+ {
24836
+ plugin: { name: "@localazy/strapi-plugin", version: pluginVersion },
24837
+ strapi: { version: strapiVersion },
24838
+ node: process.version,
24839
+ platform: process.platform,
24840
+ arch: process.arch
24841
+ },
24842
+ null,
24843
+ 2
24844
+ )
24845
+ );
24846
+ const envSnapshot = pickEnvAllowlist(process.env);
24847
+ zip.file(
24848
+ "env.json",
24849
+ JSON.stringify(
24850
+ {
24851
+ allowlist: [...DEBUG_BUNDLE_ENV_ALLOWLIST],
24852
+ values: envSnapshot
24853
+ },
24854
+ null,
24855
+ 2
24856
+ )
24857
+ );
24858
+ const { payload: browserPayload, truncationMarkers: browserMarkers } = decodeBrowserPayload(browserPayloadEncoded);
24859
+ truncationMarkers.push(...browserMarkers);
24860
+ zip.file("browser.json", JSON.stringify(browserPayload ? browserPayload : { captured: false }, null, 2));
24861
+ zip.file("session.json", JSON.stringify(sanitizeSession(session), null, 2));
24862
+ const { text: errorLogText, markers: errorLogMarkers } = buildErrorLog(session);
24863
+ truncationMarkers.push(...errorLogMarkers);
24864
+ zip.file("error.log", errorLogText);
24865
+ zip.file("plugin-settings.json", JSON.stringify(sanitizePluginSettings(pluginSettingsRaw), null, 2));
24866
+ zip.file("content-transfer-setup.json", JSON.stringify(contentTransferSetup, null, 2));
24867
+ const hostPackages = await readHostPackages(strapi2);
24868
+ if (hostPackages.data) {
24869
+ zip.file("host-packages.json", JSON.stringify(hostPackages.data, null, 2));
24870
+ }
24871
+ if (session.attemptedEntries === void 0) {
24872
+ truncationMarkers.push({ kind: "attempted-entries-missing-legacy-session" });
24873
+ }
24874
+ const selected = selectAttemptedEntries(session.attemptedEntries ?? []);
24875
+ const kept = selected.slice(0, CAPS.totalEntries);
24876
+ const skipped = selected.slice(CAPS.totalEntries);
24877
+ if (skipped.length > 0) {
24878
+ truncationMarkers.push({
24879
+ kind: "entries-skipped",
24880
+ tuples: skipped.map(({ uid, documentId, locale }) => ({ uid, documentId, locale }))
24881
+ });
24882
+ }
24883
+ const uniqueUids = [...new Set(kept.map((t3) => t3.uid))];
24884
+ for (const uid of uniqueUids) {
24885
+ const schema = serializeContentTypeSchema(strapi2.contentTypes?.[uid]);
24886
+ if (schema) {
24887
+ zip.file(`content-types/${safeUidForPath(uid)}.schema.json`, JSON.stringify(schema, null, 2));
24888
+ }
24889
+ }
24890
+ for (const tuple of kept) {
24891
+ let documentJson = null;
24892
+ try {
24893
+ const doc = await strapi2.documents(tuple.uid).findOne({ documentId: tuple.documentId, locale: tuple.locale, populate: "*" });
24894
+ documentJson = doc ? stripDocumentInternals(doc) : { __not_found: true, attemptedAt: tuple.attemptedAt };
24895
+ } catch (err) {
24896
+ documentJson = { __error: err instanceof Error ? err.message : String(err) };
24897
+ }
24898
+ const serialised = JSON.stringify(documentJson, null, 2);
24899
+ const sizeBytes = Buffer.byteLength(serialised, "utf8");
24900
+ let body;
24901
+ if (sizeBytes > CAPS.perEntryBytes) {
24902
+ truncationMarkers.push({
24903
+ kind: "entry-too-large",
24904
+ uid: tuple.uid,
24905
+ documentId: tuple.documentId,
24906
+ locale: tuple.locale,
24907
+ originalSizeBytes: sizeBytes
24908
+ });
24909
+ body = JSON.stringify({ __truncated: true, originalSizeBytes: sizeBytes }, null, 2);
24910
+ } else {
24911
+ body = serialised;
24912
+ }
24913
+ const safeDocId = tuple.documentId.replace(/[^a-zA-Z0-9_-]/g, "_");
24914
+ zip.file(`entries/${safeUidForPath(tuple.uid)}/${safeDocId}--${tuple.locale}.json`, body);
24915
+ }
24916
+ zip.file(
24917
+ "README.md",
24918
+ buildReadme({
24919
+ sessionShortId,
24920
+ sessionId: session.id,
24921
+ schemaVersion: DEBUG_BUNDLE_SCHEMA_VERSION,
24922
+ hostPackagesSkipped: !hostPackages.data
24923
+ })
24924
+ );
24925
+ if (truncationMarkers.length > 0 || !hostPackages.data) {
24926
+ const marker = {
24927
+ schemaVersion: DEBUG_BUNDLE_SCHEMA_VERSION,
24928
+ markers: truncationMarkers,
24929
+ hostPackagesSkipped: hostPackages.data ? null : hostPackages.reason ?? "unknown"
24930
+ };
24931
+ zip.file("bundle-truncated.json", JSON.stringify(marker, null, 2));
24932
+ }
24933
+ const zipBuffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
24934
+ const filename = `localazy-debug-bundle-${sessionShortId}-${formatTimestampForFilename(Date.now())}.zip`;
24935
+ return { zipBuffer, filename };
24936
+ };
24937
+ const DebugBundleController = ({ strapi: strapi2 }) => ({
24938
+ async getBundle(ctx) {
24939
+ const { sessionId } = ctx.params;
24940
+ const browserPayloadEncoded = typeof ctx.query.browser === "string" ? ctx.query.browser : null;
24941
+ try {
24942
+ const { zipBuffer, filename } = await buildDebugBundle({
24943
+ strapi: strapi2,
24944
+ sessionId,
24945
+ browserPayloadEncoded
24946
+ });
24947
+ ctx.type = "application/zip";
24948
+ ctx.set("Content-Disposition", `attachment; filename="${filename}"`);
24949
+ ctx.body = zipBuffer;
24950
+ } catch (err) {
24951
+ if (err instanceof SessionNotFoundError) {
24952
+ return ctx.notFound("Session not found");
24953
+ }
24954
+ strapi2.log.error(`[localazy] failed to build debug bundle: ${err instanceof Error ? err.message : err}`);
24955
+ throw err;
24956
+ }
24957
+ }
24958
+ });
24491
24959
  const controllers = {
24492
24960
  LocalazyUserController,
24493
24961
  LocalazyAuthController,
@@ -24496,7 +24964,8 @@ const controllers = {
24496
24964
  PluginSettingsController,
24497
24965
  LocalazyTransferController,
24498
24966
  EntryExclusionController,
24499
- ActivityLogsController
24967
+ ActivityLogsController,
24968
+ DebugBundleController
24500
24969
  };
24501
24970
  const verifyWebhook = (config2, { strapi: strapi2 }) => {
24502
24971
  return async (ctx, next) => {
@@ -24554,13 +25023,17 @@ const middlewares = {
24554
25023
  };
24555
25024
  const policies = {};
24556
25025
  const ROUTE_PREFIX$8 = "/user";
25026
+ const readPolicy$6 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25027
+ const settingsUpdatePolicy$4 = [
25028
+ { name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.SETTINGS_UPDATE] } }
25029
+ ];
24557
25030
  const LocalazyUserRoutes = [
24558
25031
  {
24559
25032
  method: "GET",
24560
25033
  path: `${ROUTE_PREFIX$8}`,
24561
25034
  handler: "LocalazyUserController.getUser",
24562
25035
  config: {
24563
- policies: []
25036
+ policies: readPolicy$6
24564
25037
  }
24565
25038
  },
24566
25039
  {
@@ -24568,7 +25041,7 @@ const LocalazyUserRoutes = [
24568
25041
  path: `${ROUTE_PREFIX$8}`,
24569
25042
  handler: "LocalazyUserController.updateUser",
24570
25043
  config: {
24571
- policies: []
25044
+ policies: settingsUpdatePolicy$4
24572
25045
  }
24573
25046
  },
24574
25047
  {
@@ -24576,18 +25049,21 @@ const LocalazyUserRoutes = [
24576
25049
  path: `${ROUTE_PREFIX$8}`,
24577
25050
  handler: "LocalazyUserController.deleteUser",
24578
25051
  config: {
24579
- policies: []
25052
+ policies: settingsUpdatePolicy$4
24580
25053
  }
24581
25054
  }
24582
25055
  ];
24583
25056
  const ROUTE_PREFIX$7 = "/auth";
25057
+ const settingsUpdatePolicy$3 = [
25058
+ { name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.SETTINGS_UPDATE] } }
25059
+ ];
24584
25060
  const LocalazyAuthRoutes = [
24585
25061
  {
24586
25062
  method: "GET",
24587
25063
  path: `${ROUTE_PREFIX$7}/generate-keys`,
24588
25064
  handler: "LocalazyAuthController.generateKeys",
24589
25065
  config: {
24590
- policies: []
25066
+ policies: settingsUpdatePolicy$3
24591
25067
  }
24592
25068
  },
24593
25069
  {
@@ -24595,18 +25071,26 @@ const LocalazyAuthRoutes = [
24595
25071
  path: `${ROUTE_PREFIX$7}/continuous-poll`,
24596
25072
  handler: "LocalazyAuthController.continuousPoll",
24597
25073
  config: {
24598
- policies: []
25074
+ policies: settingsUpdatePolicy$3
24599
25075
  }
24600
25076
  }
24601
25077
  ];
24602
25078
  const ROUTE_PREFIX$6 = "/plugin-settings";
25079
+ const readPolicy$5 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25080
+ const settingsUpdatePolicy$2 = [
25081
+ { name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.SETTINGS_UPDATE] } }
25082
+ ];
24603
25083
  const PluginSettingsRoutes = [
24604
25084
  {
24605
25085
  method: "GET",
24606
25086
  path: `${ROUTE_PREFIX$6}/content-transfer-setup`,
24607
25087
  handler: "PluginSettingsController.getContentTransferSetup",
24608
25088
  config: {
24609
- policies: []
25089
+ // Upload/Download pages call this to detect "model changed" before
25090
+ // transferring. Gating on `settings.read` would lock transfer-only
25091
+ // roles out of the transfer pages, so the read is open to any plugin
25092
+ // user. The destructive PUT below stays on `settings.update`.
25093
+ policies: readPolicy$5
24610
25094
  }
24611
25095
  },
24612
25096
  {
@@ -24614,7 +25098,7 @@ const PluginSettingsRoutes = [
24614
25098
  path: `${ROUTE_PREFIX$6}/content-transfer-setup`,
24615
25099
  handler: "PluginSettingsController.updateContentTransferSetup",
24616
25100
  config: {
24617
- policies: []
25101
+ policies: settingsUpdatePolicy$2
24618
25102
  }
24619
25103
  },
24620
25104
  {
@@ -24622,7 +25106,7 @@ const PluginSettingsRoutes = [
24622
25106
  path: `${ROUTE_PREFIX$6}/plugin-settings`,
24623
25107
  handler: "PluginSettingsController.getPluginSettings",
24624
25108
  config: {
24625
- policies: []
25109
+ policies: readPolicy$5
24626
25110
  }
24627
25111
  },
24628
25112
  {
@@ -24630,7 +25114,20 @@ const PluginSettingsRoutes = [
24630
25114
  path: `${ROUTE_PREFIX$6}/plugin-settings`,
24631
25115
  handler: "PluginSettingsController.updatePluginSettings",
24632
25116
  config: {
24633
- policies: []
25117
+ // Persists destructive Global Settings (webhookConfig, download.*, upload.*).
25118
+ // UI-pref-only writes use `PUT /plugin-settings/ui-prefs` (read-gated) below.
25119
+ policies: settingsUpdatePolicy$2
25120
+ }
25121
+ },
25122
+ {
25123
+ method: "PUT",
25124
+ path: `${ROUTE_PREFIX$6}/ui-prefs`,
25125
+ handler: "PluginSettingsController.updatePluginSettingsUiPrefs",
25126
+ config: {
25127
+ // Per-user UI state (last visited route, sort prefs). Service-side filters
25128
+ // the body to a fixed allowlist so a `read`-only caller cannot reach the
25129
+ // destructive fields that share the same plugin-settings store.
25130
+ policies: readPolicy$5
24634
25131
  }
24635
25132
  },
24636
25133
  {
@@ -24638,18 +25135,22 @@ const PluginSettingsRoutes = [
24638
25135
  path: `${ROUTE_PREFIX$6}/sync-cursor`,
24639
25136
  handler: "PluginSettingsController.getSyncCursor",
24640
25137
  config: {
24641
- policies: []
25138
+ policies: readPolicy$5
24642
25139
  }
24643
25140
  }
24644
25141
  ];
24645
25142
  const ROUTE_PREFIX$5 = "/strapi";
25143
+ const readPolicy$4 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25144
+ const settingsUpdatePolicy$1 = [
25145
+ { name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.SETTINGS_UPDATE] } }
25146
+ ];
24646
25147
  const StrapiRoutes = [
24647
25148
  {
24648
25149
  method: "GET",
24649
25150
  path: `${ROUTE_PREFIX$5}/models`,
24650
25151
  handler: "StrapiController.getModels",
24651
25152
  config: {
24652
- policies: []
25153
+ policies: readPolicy$4
24653
25154
  }
24654
25155
  },
24655
25156
  {
@@ -24657,7 +25158,7 @@ const StrapiRoutes = [
24657
25158
  path: `${ROUTE_PREFIX$5}/localizable-models`,
24658
25159
  handler: "StrapiController.getLocalizableModels",
24659
25160
  config: {
24660
- policies: []
25161
+ policies: readPolicy$4
24661
25162
  }
24662
25163
  },
24663
25164
  {
@@ -24665,7 +25166,7 @@ const StrapiRoutes = [
24665
25166
  path: `${ROUTE_PREFIX$5}/lifecycle/localazy-webhooks`,
24666
25167
  handler: "StrapiController.postLifecycleLocalazyWebhooks",
24667
25168
  config: {
24668
- policies: []
25169
+ policies: settingsUpdatePolicy$1
24669
25170
  }
24670
25171
  },
24671
25172
  {
@@ -24673,18 +25174,19 @@ const StrapiRoutes = [
24673
25174
  path: `${ROUTE_PREFIX$5}/version`,
24674
25175
  handler: "StrapiController.getPluginVersion",
24675
25176
  config: {
24676
- policies: []
25177
+ policies: readPolicy$4
24677
25178
  }
24678
25179
  }
24679
25180
  ];
24680
25181
  const ROUTE_PREFIX$4 = "/transfer";
25182
+ const transferPolicy$2 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.TRANSFER] } }];
24681
25183
  const LocalazyTransferRoutes = [
24682
25184
  {
24683
25185
  method: "POST",
24684
25186
  path: `${ROUTE_PREFIX$4}/upload`,
24685
25187
  handler: "LocalazyTransferController.upload",
24686
25188
  config: {
24687
- policies: []
25189
+ policies: transferPolicy$2
24688
25190
  }
24689
25191
  },
24690
25192
  {
@@ -24692,18 +25194,22 @@ const LocalazyTransferRoutes = [
24692
25194
  path: `${ROUTE_PREFIX$4}/download`,
24693
25195
  handler: "LocalazyTransferController.download",
24694
25196
  config: {
24695
- policies: []
25197
+ policies: transferPolicy$2
24696
25198
  }
24697
25199
  }
24698
25200
  ];
24699
25201
  const ROUTE_PREFIX$3 = "/project";
25202
+ const readPolicy$3 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25203
+ const settingsUpdatePolicy = [
25204
+ { name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.SETTINGS_UPDATE] } }
25205
+ ];
24700
25206
  const LocalazyProjectRoutes = [
24701
25207
  {
24702
25208
  method: "GET",
24703
25209
  path: `${ROUTE_PREFIX$3}`,
24704
25210
  handler: "LocalazyProjectController.getConnectedProject",
24705
25211
  config: {
24706
- policies: []
25212
+ policies: readPolicy$3
24707
25213
  }
24708
25214
  },
24709
25215
  {
@@ -24711,7 +25217,7 @@ const LocalazyProjectRoutes = [
24711
25217
  path: `${ROUTE_PREFIX$3}/webhooks`,
24712
25218
  handler: "LocalazyProjectController.getWebhookStatus",
24713
25219
  config: {
24714
- policies: []
25220
+ policies: readPolicy$3
24715
25221
  }
24716
25222
  },
24717
25223
  {
@@ -24719,7 +25225,7 @@ const LocalazyProjectRoutes = [
24719
25225
  path: `${ROUTE_PREFIX$3}/webhooks/setup`,
24720
25226
  handler: "LocalazyProjectController.setupWebhook",
24721
25227
  config: {
24722
- policies: []
25228
+ policies: settingsUpdatePolicy
24723
25229
  }
24724
25230
  },
24725
25231
  {
@@ -24727,7 +25233,7 @@ const LocalazyProjectRoutes = [
24727
25233
  path: `${ROUTE_PREFIX$3}/strapi-url`,
24728
25234
  handler: "LocalazyProjectController.getStrapiUrl",
24729
25235
  config: {
24730
- policies: []
25236
+ policies: readPolicy$3
24731
25237
  }
24732
25238
  }
24733
25239
  ];
@@ -24746,13 +25252,15 @@ const LocalazyPublicTransferRoutes = [
24746
25252
  }
24747
25253
  ];
24748
25254
  const ROUTE_PREFIX$1 = "/entry-exclusion";
25255
+ const readPolicy$2 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25256
+ const transferPolicy$1 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.TRANSFER] } }];
24749
25257
  const EntryExclusionRoutes = [
24750
25258
  {
24751
25259
  method: "GET",
24752
25260
  path: `${ROUTE_PREFIX$1}/:contentType/:documentId`,
24753
25261
  handler: "EntryExclusionController.getEntryExclusion",
24754
25262
  config: {
24755
- policies: []
25263
+ policies: readPolicy$2
24756
25264
  }
24757
25265
  },
24758
25266
  {
@@ -24760,7 +25268,7 @@ const EntryExclusionRoutes = [
24760
25268
  path: `${ROUTE_PREFIX$1}/:contentType/:documentId`,
24761
25269
  handler: "EntryExclusionController.setEntryExclusion",
24762
25270
  config: {
24763
- policies: []
25271
+ policies: transferPolicy$1
24764
25272
  }
24765
25273
  },
24766
25274
  {
@@ -24768,18 +25276,20 @@ const EntryExclusionRoutes = [
24768
25276
  path: `${ROUTE_PREFIX$1}/:contentType`,
24769
25277
  handler: "EntryExclusionController.getContentTypeExclusions",
24770
25278
  config: {
24771
- policies: []
25279
+ policies: readPolicy$2
24772
25280
  }
24773
25281
  }
24774
25282
  ];
24775
25283
  const ROUTE_PREFIX = "/activity-logs";
25284
+ const readPolicy$1 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25285
+ const transferPolicy = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.TRANSFER] } }];
24776
25286
  const ActivityLogsRoutes = [
24777
25287
  {
24778
25288
  method: "GET",
24779
25289
  path: `${ROUTE_PREFIX}`,
24780
25290
  handler: "ActivityLogsController.getSessions",
24781
25291
  config: {
24782
- policies: []
25292
+ policies: readPolicy$1
24783
25293
  }
24784
25294
  },
24785
25295
  {
@@ -24787,7 +25297,7 @@ const ActivityLogsRoutes = [
24787
25297
  path: `${ROUTE_PREFIX}/export`,
24788
25298
  handler: "ActivityLogsController.exportSessions",
24789
25299
  config: {
24790
- policies: []
25300
+ policies: readPolicy$1
24791
25301
  }
24792
25302
  },
24793
25303
  {
@@ -24795,7 +25305,7 @@ const ActivityLogsRoutes = [
24795
25305
  path: `${ROUTE_PREFIX}/:sessionId`,
24796
25306
  handler: "ActivityLogsController.getSession",
24797
25307
  config: {
24798
- policies: []
25308
+ policies: readPolicy$1
24799
25309
  }
24800
25310
  },
24801
25311
  {
@@ -24803,13 +25313,28 @@ const ActivityLogsRoutes = [
24803
25313
  path: `${ROUTE_PREFIX}`,
24804
25314
  handler: "ActivityLogsController.clearSessions",
24805
25315
  config: {
24806
- policies: []
25316
+ policies: transferPolicy
24807
25317
  }
24808
25318
  }
24809
25319
  ];
25320
+ const readPolicy = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25321
+ const DebugBundleRoutes = [
25322
+ {
25323
+ method: "GET",
25324
+ path: "/activity-logs/:sessionId/bundle",
25325
+ handler: "DebugBundleController.getBundle",
25326
+ config: {
25327
+ policies: readPolicy
25328
+ }
25329
+ }
25330
+ ];
25331
+ const withoutAdminPolicies = (routes2) => routes2.map((route) => ({
25332
+ ...route,
25333
+ config: { ...route.config, policies: [] }
25334
+ }));
24810
25335
  const routes = {
24811
25336
  "content-api": {
24812
- routes: [...StrapiRoutes]
25337
+ routes: withoutAdminPolicies(StrapiRoutes)
24813
25338
  },
24814
25339
  admin: {
24815
25340
  type: "admin",
@@ -24821,7 +25346,8 @@ const routes = {
24821
25346
  ...LocalazyTransferRoutes,
24822
25347
  ...LocalazyProjectRoutes,
24823
25348
  ...EntryExclusionRoutes,
24824
- ...ActivityLogsRoutes
25349
+ ...ActivityLogsRoutes,
25350
+ ...DebugBundleRoutes
24825
25351
  ]
24826
25352
  },
24827
25353
  LocalazyPublicTransferRoutes: {
@@ -25020,6 +25546,7 @@ const KEY$1 = "sync-cursor";
25020
25546
  const emptySyncCursor = {
25021
25547
  processedKeys: {}
25022
25548
  };
25549
+ const UI_PREF_FIELDS = ["defaultRoute", "activityLogsSort"];
25023
25550
  const PluginSettingsService = ({ strapi: strapi2 }) => ({
25024
25551
  async getContentTransferSetup() {
25025
25552
  const pluginStore = getStrapiStore(strapi2);
@@ -25056,6 +25583,20 @@ const PluginSettingsService = ({ strapi: strapi2 }) => ({
25056
25583
  });
25057
25584
  return newSettings;
25058
25585
  },
25586
+ // Counterpart to `updatePluginSettings` for the `read`-gated UI-prefs route.
25587
+ // Drops any field outside `UI_PREF_FIELDS` server-side so a `read`-only caller
25588
+ // cannot reach `webhookConfig` / `download.*` / `upload.*` via the merge.
25589
+ async updatePluginSettingsUiPrefs(prefs) {
25590
+ const sanitized = {};
25591
+ if (prefs && typeof prefs === "object") {
25592
+ for (const field of UI_PREF_FIELDS) {
25593
+ if (field in prefs) {
25594
+ sanitized[field] = prefs[field];
25595
+ }
25596
+ }
25597
+ }
25598
+ return this.updatePluginSettings(sanitized);
25599
+ },
25059
25600
  async getSyncCursor() {
25060
25601
  const pluginStore = getStrapiStore(strapi2);
25061
25602
  const cursor = await pluginStore.get({ key: KEY$1 });
@@ -25100,6 +25641,8 @@ const LocalazyTransferUploadService = ({ strapi: strapi2 }) => ({
25100
25641
  const { setup: setup2 } = contentTransferSetup;
25101
25642
  const collectionsNames = getCollectionsNames(setup2);
25102
25643
  const models = await StrapiService2.getModels();
25644
+ const strapiDefaultLocaleCode = await StrapiI18nService2.getDefaultLocaleCode();
25645
+ const attemptedTuples = [];
25103
25646
  let pickedFlattenStrapiContent = {};
25104
25647
  for (const collectionName of collectionsNames) {
25105
25648
  const currentModel = models.find((model) => model.collectionName === collectionName);
@@ -25162,6 +25705,15 @@ const LocalazyTransferUploadService = ({ strapi: strapi2 }) => ({
25162
25705
  });
25163
25706
  await activityLogsService.addEntry(sessionId, message);
25164
25707
  strapi2.log.info(message);
25708
+ if (strapiDefaultLocaleCode) {
25709
+ for (const entry of entries) {
25710
+ attemptedTuples.push({
25711
+ uid: modelUid,
25712
+ documentId: entry.documentId,
25713
+ locale: strapiDefaultLocaleCode
25714
+ });
25715
+ }
25716
+ }
25165
25717
  entries = omitDeep(entries, ["locale", "createdAt", "updatedAt", "publishedAt"]);
25166
25718
  entries.forEach((entry) => {
25167
25719
  const flatten = flattenObject({
@@ -25179,7 +25731,6 @@ const LocalazyTransferUploadService = ({ strapi: strapi2 }) => ({
25179
25731
  ...characterLimitsMetadata
25180
25732
  };
25181
25733
  }
25182
- const strapiDefaultLocaleCode = await StrapiI18nService2.getDefaultLocaleCode();
25183
25734
  const localazyLocaleCode = strapiDefaultLocaleCode ? isoStrapiToLocalazy(strapiDefaultLocaleCode) : config.default.LOCALAZY_DEFAULT_LOCALE;
25184
25735
  const importFile = LocalazyUploadService2.createImportFileRepresentation(
25185
25736
  localazyLocaleCode,
@@ -25201,6 +25752,10 @@ const LocalazyTransferUploadService = ({ strapi: strapi2 }) => ({
25201
25752
  await activityLogsService.addEntry(sessionId, "Uploading collections to Localazy...");
25202
25753
  try {
25203
25754
  await LocalazyUploadService2.upload(importFile, uploadConfig);
25755
+ await activityLogsService.recordAttemptedEntries(
25756
+ sessionId,
25757
+ attemptedTuples.map((t3) => ({ ...t3, status: "success" }))
25758
+ );
25204
25759
  } catch (error) {
25205
25760
  success = false;
25206
25761
  const errorMessage = `Upload to Localazy failed: ${error instanceof Error ? error.message : String(error)}`;
@@ -25210,6 +25765,10 @@ const LocalazyTransferUploadService = ({ strapi: strapi2 }) => ({
25210
25765
  message: errorMessage
25211
25766
  });
25212
25767
  await activityLogsService.addEntry(sessionId, errorMessage);
25768
+ await activityLogsService.recordAttemptedEntries(
25769
+ sessionId,
25770
+ attemptedTuples.map((t3) => ({ ...t3, status: "failed", errorMessage }))
25771
+ );
25213
25772
  await activityLogsService.finishSession(sessionId, "failed", errorMessage);
25214
25773
  console.timeEnd("upload");
25215
25774
  return;
@@ -25614,7 +26173,7 @@ const shouldSetDownloadedProperty = (modelContentTransferSetup, parsedKeyRest) =
25614
26173
  return "no";
25615
26174
  }
25616
26175
  };
25617
- const getFilteredLanguagesCodesForDownload = async (languagesCodes) => {
26176
+ const getFilteredLanguagesCodesForDownload = async (languagesCodes, requestedUiLanguages) => {
25618
26177
  const pluginSettingsServiceHelper = new PluginSettingsServiceHelper();
25619
26178
  const requestInitiatorHelper = new RequestInitiatorHelper(strapi);
25620
26179
  await pluginSettingsServiceHelper.setup();
@@ -25622,7 +26181,7 @@ const getFilteredLanguagesCodesForDownload = async (languagesCodes) => {
25622
26181
  if (requestInitiatorHelper.isInitiatedByLocalazyWebhook()) {
25623
26182
  localLanguagesCodes = pluginSettingsServiceHelper.getWebhookLanguagesCodes();
25624
26183
  } else if (requestInitiatorHelper.isInitiatedByLocalazyPluginUI()) {
25625
- localLanguagesCodes = pluginSettingsServiceHelper.getUiLanguagesCodes();
26184
+ localLanguagesCodes = requestedUiLanguages ?? pluginSettingsServiceHelper.getUiLanguagesCodes();
25626
26185
  } else {
25627
26186
  strapi.log.warn("Called by an unknown initiator.");
25628
26187
  return languagesCodes;
@@ -25635,7 +26194,8 @@ const getFilteredLanguagesCodesForDownload = async (languagesCodes) => {
25635
26194
  const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25636
26195
  async download({
25637
26196
  notificationService,
25638
- fullSync = false
26197
+ fullSync = false,
26198
+ requestedUiLanguages
25639
26199
  }) {
25640
26200
  console.time("download");
25641
26201
  const syncMode = fullSync ? "Full sync" : "Incremental sync";
@@ -25715,7 +26275,7 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25715
26275
  return;
25716
26276
  }
25717
26277
  let languagesCodes = projectLanguagesWithoutSourceLanguage.map((language3) => language3.code);
25718
- languagesCodes = await getFilteredLanguagesCodesForDownload(languagesCodes);
26278
+ languagesCodes = await getFilteredLanguagesCodesForDownload(languagesCodes, requestedUiLanguages);
25719
26279
  const strapiUnsupportedLanguages = [];
25720
26280
  const strapiLocales = await StrapiI18nService2.getLocales();
25721
26281
  for (const isoLocalazy of languagesCodes) {
@@ -25934,6 +26494,12 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25934
26494
  message
25935
26495
  });
25936
26496
  await activityLogsService.addEntry(sessionId, message);
26497
+ await activityLogsService.recordAttemptedEntry(sessionId, {
26498
+ uid,
26499
+ documentId,
26500
+ locale: isoStrapi,
26501
+ status: "success"
26502
+ });
25937
26503
  await markKeysProcessed(sourceKeys);
25938
26504
  } catch (e2) {
25939
26505
  success = false;
@@ -25950,6 +26516,13 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25950
26516
  sessionId,
25951
26517
  `Failed to create ${uid} [${documentId}] in ${isoStrapi}: ${e2.message}`
25952
26518
  );
26519
+ await activityLogsService.recordAttemptedEntry(sessionId, {
26520
+ uid,
26521
+ documentId,
26522
+ locale: isoStrapi,
26523
+ status: "failed",
26524
+ errorMessage: e2.message
26525
+ });
25953
26526
  }
25954
26527
  } else {
25955
26528
  try {
@@ -25968,6 +26541,12 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25968
26541
  message
25969
26542
  });
25970
26543
  await activityLogsService.addEntry(sessionId, message);
26544
+ await activityLogsService.recordAttemptedEntry(sessionId, {
26545
+ uid,
26546
+ documentId,
26547
+ locale: isoStrapi,
26548
+ status: "success"
26549
+ });
25971
26550
  await markKeysProcessed(sourceKeys);
25972
26551
  } catch (e2) {
25973
26552
  success = false;
@@ -25984,6 +26563,13 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25984
26563
  sessionId,
25985
26564
  `Failed to update ${uid} [${documentId}] in ${isoStrapi}: ${e2.message}`
25986
26565
  );
26566
+ await activityLogsService.recordAttemptedEntry(sessionId, {
26567
+ uid,
26568
+ documentId,
26569
+ locale: isoStrapi,
26570
+ status: "failed",
26571
+ errorMessage: e2.message
26572
+ });
25987
26573
  }
25988
26574
  }
25989
26575
  } catch (e2) {
@@ -25996,6 +26582,13 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25996
26582
  sessionId,
25997
26583
  `Error processing ${uid} [${documentId}] in ${isoStrapi}: ${e2.message}`
25998
26584
  );
26585
+ await activityLogsService.recordAttemptedEntry(sessionId, {
26586
+ uid,
26587
+ documentId,
26588
+ locale: isoStrapi,
26589
+ status: "failed",
26590
+ errorMessage: e2.message
26591
+ });
25999
26592
  }
26000
26593
  }
26001
26594
  }
@@ -26746,6 +27339,7 @@ const emptyActivityLogs = {
26746
27339
  sessions: []
26747
27340
  };
26748
27341
  const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
27342
+ const ATTEMPTED_ENTRIES_CAP = 5e3;
26749
27343
  let writeLock = Promise.resolve();
26750
27344
  const withLock = async (fn) => {
26751
27345
  let release;
@@ -26810,6 +27404,38 @@ const ActivityLogsService = ({ strapi: strapi2 }) => ({
26810
27404
  await this.saveActivityLogs(logs);
26811
27405
  });
26812
27406
  },
27407
+ async recordAttemptedEntry(sessionId, entry) {
27408
+ return this.recordAttemptedEntries(sessionId, [entry]);
27409
+ },
27410
+ async recordAttemptedEntries(sessionId, entries) {
27411
+ if (!entries.length) {
27412
+ return;
27413
+ }
27414
+ return withLock(async () => {
27415
+ const logs = await this.getActivityLogs();
27416
+ const session = logs.sessions.find((s2) => s2.id === sessionId);
27417
+ if (!session) {
27418
+ return;
27419
+ }
27420
+ if (!session.attemptedEntries) {
27421
+ session.attemptedEntries = [];
27422
+ }
27423
+ const now = Date.now();
27424
+ const wasAtCap = session.attemptedEntries.length >= ATTEMPTED_ENTRIES_CAP;
27425
+ const remaining = Math.max(0, ATTEMPTED_ENTRIES_CAP - session.attemptedEntries.length);
27426
+ const accepted = entries.slice(0, remaining);
27427
+ const dropped = entries.length - accepted.length;
27428
+ for (const entry of accepted) {
27429
+ session.attemptedEntries.push({ ...entry, attemptedAt: now });
27430
+ }
27431
+ if (dropped > 0 && !wasAtCap) {
27432
+ strapi2.log.warn(
27433
+ `[localazy] attemptedEntries cap (${ATTEMPTED_ENTRIES_CAP}) reached for session ${sessionId}; ${dropped} record(s) dropped.`
27434
+ );
27435
+ }
27436
+ await this.saveActivityLogs(logs);
27437
+ });
27438
+ },
26813
27439
  async finishSession(sessionId, status, summary) {
26814
27440
  return withLock(async () => {
26815
27441
  const logs = await this.getActivityLogs();