@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
@@ -10,6 +10,7 @@ const require$$0$2 = require("crypto");
10
10
  const require$$0$3 = require("events");
11
11
  const require$$2 = require("timers");
12
12
  const require$$8 = require("ws");
13
+ const JSZip = require("jszip");
13
14
  const _interopDefault = (e2) => e2 && e2.__esModule ? e2 : { default: e2 };
14
15
  const require$$0__default$2 = /* @__PURE__ */ _interopDefault(require$$0$4);
15
16
  const require$$1__default$4 = /* @__PURE__ */ _interopDefault(require$$1$4);
@@ -22,6 +23,7 @@ const require$$0__default = /* @__PURE__ */ _interopDefault(require$$0$2);
22
23
  const require$$0__default$1 = /* @__PURE__ */ _interopDefault(require$$0$3);
23
24
  const require$$2__default = /* @__PURE__ */ _interopDefault(require$$2);
24
25
  const require$$8__default = /* @__PURE__ */ _interopDefault(require$$8);
26
+ const JSZip__default = /* @__PURE__ */ _interopDefault(JSZip);
25
27
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
26
28
  function getDefaultExportFromCjs(x2) {
27
29
  return x2 && x2.__esModule && Object.prototype.hasOwnProperty.call(x2, "default") ? x2["default"] : x2;
@@ -20532,7 +20534,7 @@ var distExports = dist$1.exports;
20532
20534
  const io = /* @__PURE__ */ getDefaultExportFromCjs(distExports);
20533
20535
  const { Server: Server2, Namespace, Socket: Socket3 } = io;
20534
20536
  const name = "@localazy/strapi-plugin";
20535
- const version = "1.3.0";
20537
+ const version = "1.6.0";
20536
20538
  const description = "The official Strapi Plugin by Localazy.";
20537
20539
  const keywords = [
20538
20540
  "strapi",
@@ -20602,6 +20604,7 @@ const scripts = {
20602
20604
  prepare: "husky || true"
20603
20605
  };
20604
20606
  const dependencies = {
20607
+ jszip: "^3.10.1",
20605
20608
  shelljs: "^0.8.5",
20606
20609
  ws: "^8.18.0"
20607
20610
  };
@@ -20715,7 +20718,60 @@ const _package$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePr
20715
20718
  version
20716
20719
  }, Symbol.toStringTag, { value: "Module" }));
20717
20720
  const PLUGIN_NAME = strapi$1.name;
20718
- const bootstrap = ({ strapi: strapi2 }) => {
20721
+ const PERMISSION_UIDS = {
20722
+ READ: `plugin::${PLUGIN_NAME}.read`,
20723
+ TRANSFER: `plugin::${PLUGIN_NAME}.transfer`,
20724
+ SETTINGS_UPDATE: `plugin::${PLUGIN_NAME}.settings.update`
20725
+ };
20726
+ const LOCALAZY_RBAC_ACTIONS = [
20727
+ {
20728
+ section: "plugins",
20729
+ subCategory: "General",
20730
+ pluginName: PLUGIN_NAME,
20731
+ displayName: "Read",
20732
+ uid: "read"
20733
+ },
20734
+ {
20735
+ section: "plugins",
20736
+ subCategory: "General",
20737
+ pluginName: PLUGIN_NAME,
20738
+ displayName: "Transfer",
20739
+ uid: "transfer"
20740
+ },
20741
+ {
20742
+ section: "plugins",
20743
+ subCategory: "Settings",
20744
+ pluginName: PLUGIN_NAME,
20745
+ displayName: "Read settings",
20746
+ uid: "settings.read"
20747
+ },
20748
+ {
20749
+ section: "plugins",
20750
+ subCategory: "Settings",
20751
+ pluginName: PLUGIN_NAME,
20752
+ displayName: "Update settings",
20753
+ uid: "settings.update"
20754
+ }
20755
+ ];
20756
+ const registerRbacActions = async (strapi2) => {
20757
+ try {
20758
+ const permissionService = strapi2.service("admin::permission");
20759
+ const actionProvider = permissionService?.actionProvider;
20760
+ if (!actionProvider?.registerMany) {
20761
+ strapi2.log.warn(
20762
+ `[${PLUGIN_NAME}] admin::permission.actionProvider unavailable — skipping RBAC action registration`
20763
+ );
20764
+ return;
20765
+ }
20766
+ await actionProvider.registerMany(LOCALAZY_RBAC_ACTIONS);
20767
+ } catch (error) {
20768
+ strapi2.log.error(
20769
+ `[${PLUGIN_NAME}] Failed to register RBAC actions — plugin permissions will not appear in role editor: ${error instanceof Error ? error.message : String(error)}`
20770
+ );
20771
+ }
20772
+ };
20773
+ const bootstrap = async ({ strapi: strapi2 }) => {
20774
+ await registerRbacActions(strapi2);
20719
20775
  if (strapi2.plugin(PLUGIN_NAME)?.config("enableSocketIO", true)) {
20720
20776
  process.nextTick(async () => {
20721
20777
  const io2 = new Server2(strapi2.server.httpServer, {
@@ -24298,6 +24354,9 @@ const PluginSettingsController = ({ strapi: _strapi }) => ({
24298
24354
  async updatePluginSettings(ctx) {
24299
24355
  ctx.body = await getPluginSettingsService().updatePluginSettings(ctx.request.body);
24300
24356
  },
24357
+ async updatePluginSettingsUiPrefs(ctx) {
24358
+ ctx.body = await getPluginSettingsService().updatePluginSettingsUiPrefs(ctx.request.body);
24359
+ },
24301
24360
  async getSyncCursor(ctx) {
24302
24361
  ctx.body = await getPluginSettingsService().getSyncCursor();
24303
24362
  }
@@ -24397,6 +24456,8 @@ const LocalazyTransferController = ({ strapi: strapi2 }) => ({
24397
24456
  try {
24398
24457
  const LocalazyTransferDownloadService2 = getLocalazyTransferDownloadService();
24399
24458
  let fullSync = ctx.request.body?.fullSync === true;
24459
+ const rawUiLanguages = ctx.request.body?.uiLanguages;
24460
+ const requestedUiLanguages = Array.isArray(rawUiLanguages) && rawUiLanguages.every((code) => typeof code === "string") ? rawUiLanguages : void 0;
24400
24461
  const requestInitiatorHelper = new RequestInitiatorHelper(strapi2);
24401
24462
  if (requestInitiatorHelper.isInitiatedByLocalazyWebhook()) {
24402
24463
  const pluginSettingsServiceHelper = new PluginSettingsServiceHelper();
@@ -24406,7 +24467,8 @@ const LocalazyTransferController = ({ strapi: strapi2 }) => ({
24406
24467
  setTimeout(
24407
24468
  () => LocalazyTransferDownloadService2.download({
24408
24469
  notificationService: JobNotificationService$1,
24409
- fullSync
24470
+ fullSync,
24471
+ requestedUiLanguages
24410
24472
  }),
24411
24473
  1e3
24412
24474
  );
@@ -24501,6 +24563,413 @@ const ActivityLogsController = ({ strapi: _strapi }) => ({
24501
24563
  ctx.body = { success: true };
24502
24564
  }
24503
24565
  });
24566
+ const URL_SECRET_KEYS = ["token", "secret", "signature", "key", "apikey", "api_key"];
24567
+ const DEBUG_BUNDLE_ENV_ALLOWLIST = [
24568
+ "STRAPI_ADMIN_LOCALAZY_ENV",
24569
+ "STRAPI_ADMIN_BACKEND_URL",
24570
+ "NODE_ENV",
24571
+ "STRAPI_TELEMETRY_DISABLED"
24572
+ ];
24573
+ const redactIdentity = (identity2) => {
24574
+ if (!identity2) return null;
24575
+ return {
24576
+ userId: identity2.user?.id ?? "",
24577
+ name: identity2.user?.name ?? "",
24578
+ scope: identity2.scope ?? "",
24579
+ hasAccessToken: Boolean(identity2.accessToken)
24580
+ };
24581
+ };
24582
+ const scrubUrlSecrets = (input) => {
24583
+ if (typeof input !== "string") return input;
24584
+ if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(input) && !input.includes("?")) return input;
24585
+ try {
24586
+ const url2 = new URL(input);
24587
+ let mutated = false;
24588
+ for (const k2 of [...url2.searchParams.keys()]) {
24589
+ if (URL_SECRET_KEYS.includes(k2.toLowerCase())) {
24590
+ url2.searchParams.delete(k2);
24591
+ mutated = true;
24592
+ }
24593
+ }
24594
+ return mutated ? url2.toString() : input;
24595
+ } catch {
24596
+ return input;
24597
+ }
24598
+ };
24599
+ const pickEnvAllowlist = (env) => {
24600
+ const out = {};
24601
+ for (const key of DEBUG_BUNDLE_ENV_ALLOWLIST) {
24602
+ const raw = env[key];
24603
+ const present = raw !== void 0;
24604
+ out[key] = {
24605
+ present,
24606
+ value: present ? raw : null
24607
+ };
24608
+ }
24609
+ return out;
24610
+ };
24611
+ const serializeContentTypeSchema = (contentType) => {
24612
+ if (!contentType || typeof contentType !== "object") return null;
24613
+ const ct = contentType;
24614
+ return {
24615
+ kind: ct.kind,
24616
+ collectionName: ct.collectionName,
24617
+ info: ct.info,
24618
+ options: ct.options,
24619
+ pluginOptions: ct.pluginOptions,
24620
+ attributes: ct.attributes
24621
+ };
24622
+ };
24623
+ const DOCUMENT_INTERNAL_FIELDS = ["createdBy", "updatedBy"];
24624
+ const stripDocumentInternals = (doc) => {
24625
+ if (!doc || typeof doc !== "object") return doc;
24626
+ const clone = { ...doc };
24627
+ for (const field of DOCUMENT_INTERNAL_FIELDS) {
24628
+ delete clone[field];
24629
+ }
24630
+ return clone;
24631
+ };
24632
+ class SessionNotFoundError extends Error {
24633
+ constructor(sessionId) {
24634
+ super(`Session ${sessionId} not found`);
24635
+ this.name = "SessionNotFoundError";
24636
+ }
24637
+ }
24638
+ const DEBUG_BUNDLE_SCHEMA_VERSION = 1;
24639
+ const CAPS = {
24640
+ perEntryBytes: 1 * 1024 * 1024,
24641
+ // 1 MB
24642
+ totalEntries: 100,
24643
+ errorLogBytes: 2 * 1024 * 1024,
24644
+ // 2 MB
24645
+ consoleErrorsLines: 200,
24646
+ browserPayloadBytes: 64 * 1024
24647
+ // 64 KB
24648
+ };
24649
+ const selectAttemptedEntries = (attempted) => {
24650
+ const failedByKey = /* @__PURE__ */ new Map();
24651
+ const successByKey = /* @__PURE__ */ new Map();
24652
+ for (const record of attempted) {
24653
+ const key = `${record.uid}|${record.documentId}|${record.locale}`;
24654
+ const selected = {
24655
+ uid: record.uid,
24656
+ documentId: record.documentId,
24657
+ locale: record.locale,
24658
+ status: record.status,
24659
+ attemptedAt: record.attemptedAt
24660
+ };
24661
+ if (record.status === "failed") {
24662
+ successByKey.delete(key);
24663
+ if (!failedByKey.has(key)) failedByKey.set(key, selected);
24664
+ } else if (!failedByKey.has(key) && !successByKey.has(key)) {
24665
+ successByKey.set(key, selected);
24666
+ }
24667
+ }
24668
+ return [...failedByKey.values(), ...successByKey.values()];
24669
+ };
24670
+ const formatTimestampForFilename = (ms2) => new Date(ms2).toISOString().replace(/[:.]/g, "-");
24671
+ const safeUidForPath = (uid) => uid.replace(/::/g, "--").replace(/\./g, "-");
24672
+ const decodeBrowserPayload = (encoded) => {
24673
+ const markers = [];
24674
+ if (!encoded) return { payload: null, truncationMarkers: markers };
24675
+ try {
24676
+ const raw = Buffer.from(encoded, "base64").toString("utf8");
24677
+ const rawBytes = Buffer.byteLength(raw, "utf8");
24678
+ if (rawBytes > CAPS.browserPayloadBytes) {
24679
+ markers.push({ kind: "browser-payload-truncated", originalSizeBytes: rawBytes });
24680
+ return { payload: null, truncationMarkers: markers };
24681
+ }
24682
+ const parsed = JSON.parse(raw);
24683
+ if (parsed && Array.isArray(parsed.consoleErrors) && parsed.consoleErrors.length > CAPS.consoleErrorsLines) {
24684
+ markers.push({ kind: "console-errors-truncated", originalCount: parsed.consoleErrors.length });
24685
+ parsed.consoleErrors = parsed.consoleErrors.slice(0, CAPS.consoleErrorsLines);
24686
+ }
24687
+ return { payload: parsed, truncationMarkers: markers };
24688
+ } catch {
24689
+ return { payload: null, truncationMarkers: markers };
24690
+ }
24691
+ };
24692
+ const deepScrubUrls = (value) => {
24693
+ if (value == null) return value;
24694
+ if (typeof value === "string") return scrubUrlSecrets(value);
24695
+ if (Array.isArray(value)) return value.map(deepScrubUrls);
24696
+ if (typeof value === "object") {
24697
+ const out = {};
24698
+ for (const [k2, v2] of Object.entries(value)) {
24699
+ out[k2] = deepScrubUrls(v2);
24700
+ }
24701
+ return out;
24702
+ }
24703
+ return value;
24704
+ };
24705
+ const sanitizePluginSettings = (settings) => {
24706
+ const cloned = JSON.parse(JSON.stringify(settings ?? {}));
24707
+ if (cloned.webhookConfig?.url) {
24708
+ cloned.webhookConfig.url = scrubUrlSecrets(cloned.webhookConfig.url);
24709
+ }
24710
+ return cloned;
24711
+ };
24712
+ const sanitizeSession = (session) => {
24713
+ const cloned = JSON.parse(JSON.stringify(session));
24714
+ for (const entry of cloned.entries ?? []) {
24715
+ if (entry.data !== void 0) {
24716
+ entry.data = deepScrubUrls(entry.data);
24717
+ }
24718
+ }
24719
+ return cloned;
24720
+ };
24721
+ const buildErrorLog = (session) => {
24722
+ const lines = [];
24723
+ for (const entry of session.entries ?? []) {
24724
+ const ts = Number.isFinite(entry.timestamp) ? new Date(entry.timestamp).toISOString() : "unknown";
24725
+ lines.push(`${ts} ${entry.message}`);
24726
+ }
24727
+ if (session.status === "failed" && session.summary) {
24728
+ lines.push(`ERROR: ${session.summary}`);
24729
+ }
24730
+ const full = lines.join("\n");
24731
+ const fullBytes = Buffer.byteLength(full, "utf8");
24732
+ if (fullBytes <= CAPS.errorLogBytes) {
24733
+ return { text: full, markers: [] };
24734
+ }
24735
+ const truncated = Buffer.from(full, "utf8").subarray(fullBytes - CAPS.errorLogBytes).toString("utf8");
24736
+ return {
24737
+ text: `...[truncated, kept last ${CAPS.errorLogBytes} bytes]
24738
+ ${truncated}`,
24739
+ markers: [{ kind: "error-log-truncated", originalSizeBytes: fullBytes }]
24740
+ };
24741
+ };
24742
+ const readHostPackages = async (strapi2) => {
24743
+ const appRoot = strapi2?.dirs?.app?.root ?? process.cwd();
24744
+ if (!appRoot) return { data: null, reason: "host-app-root-unknown" };
24745
+ try {
24746
+ const raw = await require$$1$4.promises.readFile(require$$1__default.default.join(appRoot, "package.json"), "utf8");
24747
+ const parsed = JSON.parse(raw);
24748
+ return {
24749
+ data: {
24750
+ name: parsed.name,
24751
+ version: parsed.version,
24752
+ dependencies: parsed.dependencies ?? {},
24753
+ devDependencies: parsed.devDependencies ?? {}
24754
+ }
24755
+ };
24756
+ } catch (err) {
24757
+ return { data: null, reason: err instanceof Error ? err.message : String(err) };
24758
+ }
24759
+ };
24760
+ const buildReadme = ({
24761
+ sessionShortId,
24762
+ sessionId,
24763
+ schemaVersion,
24764
+ hostPackagesSkipped
24765
+ }) => {
24766
+ const lines = [];
24767
+ lines.push(`# Localazy Strapi Plugin — Troubleshooting Bundle`);
24768
+ lines.push("");
24769
+ lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
24770
+ lines.push(`Session id: ${sessionId}`);
24771
+ lines.push(`Schema version: ${schemaVersion}`);
24772
+ lines.push("");
24773
+ lines.push("## Contents");
24774
+ lines.push("");
24775
+ lines.push("- `meta.json` — schema version, session metadata.");
24776
+ lines.push("- `versions.json` — plugin version, Strapi version, Node version, platform/arch.");
24777
+ lines.push("- `env.json` — allow-listed environment variables only.");
24778
+ lines.push("- `browser.json` — user agent, language, viewport, console errors (from the admin UI).");
24779
+ lines.push("- `session.json` — full activity-log session record (URLs scrubbed of secret query keys).");
24780
+ lines.push("- `error.log` — chronological text log of session entries.");
24781
+ lines.push("- `plugin-settings.json` — full plugin settings (webhook URL secrets scrubbed).");
24782
+ lines.push("- `content-transfer-setup.json` — content-transfer setup record.");
24783
+ if (hostPackagesSkipped) {
24784
+ lines.push("- `host-packages.json` — NOT INCLUDED (could not be read; see `bundle-truncated.json`).");
24785
+ } else {
24786
+ lines.push("- `host-packages.json` — host project's package.json (deps + devDeps only).");
24787
+ }
24788
+ lines.push("- `content-types/` — schema.json per touched content-type UID.");
24789
+ lines.push("- `entries/<uid>/<documentId>--<locale>.json` — per-entry payload from `strapi.documents().findOne()`.");
24790
+ lines.push("- `bundle-truncated.json` — present only if any per-section cap was exceeded.");
24791
+ lines.push("");
24792
+ lines.push("## Reproducing locally");
24793
+ lines.push("");
24794
+ lines.push("1. `unzip` the bundle.");
24795
+ lines.push("2. Recreate the content types from `content-types/*.schema.json` in your Strapi project.");
24796
+ lines.push("3. Import the entries via the Document Service using `entries/<uid>/*.json`.");
24797
+ lines.push("4. Apply `plugin-settings.json` and `content-transfer-setup.json` via the plugin admin UI.");
24798
+ lines.push("5. Trigger the same upload/download/webhook with the same locale set.");
24799
+ lines.push("6. Compare the failure against `error.log` and `session.json`.");
24800
+ lines.push("");
24801
+ lines.push(`Bundle short id: ${sessionShortId}`);
24802
+ return lines.join("\n");
24803
+ };
24804
+ const buildDebugBundle = async ({
24805
+ strapi: strapi2,
24806
+ sessionId,
24807
+ browserPayloadEncoded
24808
+ }) => {
24809
+ const zip = new JSZip__default.default();
24810
+ const truncationMarkers = [];
24811
+ const pluginName = "localazy";
24812
+ const activityLogsService = strapi2.plugin(pluginName).service("ActivityLogsService");
24813
+ const pluginSettingsService = strapi2.plugin(pluginName).service("PluginSettingsService");
24814
+ const strapiService = strapi2.plugin(pluginName).service("StrapiService");
24815
+ const localazyUserService = strapi2.plugin(pluginName).service("LocalazyUserService");
24816
+ const session = await activityLogsService.getSession(sessionId);
24817
+ if (!session) {
24818
+ throw new SessionNotFoundError(sessionId);
24819
+ }
24820
+ const sessionShortId = session.id.slice(0, 8);
24821
+ const pluginSettingsRaw = await pluginSettingsService.getPluginSettings();
24822
+ const contentTransferSetup = await pluginSettingsService.getContentTransferSetup();
24823
+ const identity2 = await localazyUserService.getUser();
24824
+ const redactedIdentity = redactIdentity(identity2);
24825
+ const pluginVersion = await strapiService.getPluginVersion();
24826
+ const strapiVersion = strapiService.getStrapiVersion();
24827
+ zip.file(
24828
+ "meta.json",
24829
+ JSON.stringify(
24830
+ {
24831
+ schemaVersion: DEBUG_BUNDLE_SCHEMA_VERSION,
24832
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
24833
+ sessionId: session.id,
24834
+ sessionShortId,
24835
+ eventType: session.eventType,
24836
+ status: session.status,
24837
+ startedAt: session.startedAt,
24838
+ finishedAt: session.finishedAt ?? null,
24839
+ initiatedBy: session.initiatedBy,
24840
+ identity: redactedIdentity
24841
+ },
24842
+ null,
24843
+ 2
24844
+ )
24845
+ );
24846
+ zip.file(
24847
+ "versions.json",
24848
+ JSON.stringify(
24849
+ {
24850
+ plugin: { name: "@localazy/strapi-plugin", version: pluginVersion },
24851
+ strapi: { version: strapiVersion },
24852
+ node: process.version,
24853
+ platform: process.platform,
24854
+ arch: process.arch
24855
+ },
24856
+ null,
24857
+ 2
24858
+ )
24859
+ );
24860
+ const envSnapshot = pickEnvAllowlist(process.env);
24861
+ zip.file(
24862
+ "env.json",
24863
+ JSON.stringify(
24864
+ {
24865
+ allowlist: [...DEBUG_BUNDLE_ENV_ALLOWLIST],
24866
+ values: envSnapshot
24867
+ },
24868
+ null,
24869
+ 2
24870
+ )
24871
+ );
24872
+ const { payload: browserPayload, truncationMarkers: browserMarkers } = decodeBrowserPayload(browserPayloadEncoded);
24873
+ truncationMarkers.push(...browserMarkers);
24874
+ zip.file("browser.json", JSON.stringify(browserPayload ? browserPayload : { captured: false }, null, 2));
24875
+ zip.file("session.json", JSON.stringify(sanitizeSession(session), null, 2));
24876
+ const { text: errorLogText, markers: errorLogMarkers } = buildErrorLog(session);
24877
+ truncationMarkers.push(...errorLogMarkers);
24878
+ zip.file("error.log", errorLogText);
24879
+ zip.file("plugin-settings.json", JSON.stringify(sanitizePluginSettings(pluginSettingsRaw), null, 2));
24880
+ zip.file("content-transfer-setup.json", JSON.stringify(contentTransferSetup, null, 2));
24881
+ const hostPackages = await readHostPackages(strapi2);
24882
+ if (hostPackages.data) {
24883
+ zip.file("host-packages.json", JSON.stringify(hostPackages.data, null, 2));
24884
+ }
24885
+ if (session.attemptedEntries === void 0) {
24886
+ truncationMarkers.push({ kind: "attempted-entries-missing-legacy-session" });
24887
+ }
24888
+ const selected = selectAttemptedEntries(session.attemptedEntries ?? []);
24889
+ const kept = selected.slice(0, CAPS.totalEntries);
24890
+ const skipped = selected.slice(CAPS.totalEntries);
24891
+ if (skipped.length > 0) {
24892
+ truncationMarkers.push({
24893
+ kind: "entries-skipped",
24894
+ tuples: skipped.map(({ uid, documentId, locale }) => ({ uid, documentId, locale }))
24895
+ });
24896
+ }
24897
+ const uniqueUids = [...new Set(kept.map((t3) => t3.uid))];
24898
+ for (const uid of uniqueUids) {
24899
+ const schema = serializeContentTypeSchema(strapi2.contentTypes?.[uid]);
24900
+ if (schema) {
24901
+ zip.file(`content-types/${safeUidForPath(uid)}.schema.json`, JSON.stringify(schema, null, 2));
24902
+ }
24903
+ }
24904
+ for (const tuple of kept) {
24905
+ let documentJson = null;
24906
+ try {
24907
+ const doc = await strapi2.documents(tuple.uid).findOne({ documentId: tuple.documentId, locale: tuple.locale, populate: "*" });
24908
+ documentJson = doc ? stripDocumentInternals(doc) : { __not_found: true, attemptedAt: tuple.attemptedAt };
24909
+ } catch (err) {
24910
+ documentJson = { __error: err instanceof Error ? err.message : String(err) };
24911
+ }
24912
+ const serialised = JSON.stringify(documentJson, null, 2);
24913
+ const sizeBytes = Buffer.byteLength(serialised, "utf8");
24914
+ let body;
24915
+ if (sizeBytes > CAPS.perEntryBytes) {
24916
+ truncationMarkers.push({
24917
+ kind: "entry-too-large",
24918
+ uid: tuple.uid,
24919
+ documentId: tuple.documentId,
24920
+ locale: tuple.locale,
24921
+ originalSizeBytes: sizeBytes
24922
+ });
24923
+ body = JSON.stringify({ __truncated: true, originalSizeBytes: sizeBytes }, null, 2);
24924
+ } else {
24925
+ body = serialised;
24926
+ }
24927
+ const safeDocId = tuple.documentId.replace(/[^a-zA-Z0-9_-]/g, "_");
24928
+ zip.file(`entries/${safeUidForPath(tuple.uid)}/${safeDocId}--${tuple.locale}.json`, body);
24929
+ }
24930
+ zip.file(
24931
+ "README.md",
24932
+ buildReadme({
24933
+ sessionShortId,
24934
+ sessionId: session.id,
24935
+ schemaVersion: DEBUG_BUNDLE_SCHEMA_VERSION,
24936
+ hostPackagesSkipped: !hostPackages.data
24937
+ })
24938
+ );
24939
+ if (truncationMarkers.length > 0 || !hostPackages.data) {
24940
+ const marker = {
24941
+ schemaVersion: DEBUG_BUNDLE_SCHEMA_VERSION,
24942
+ markers: truncationMarkers,
24943
+ hostPackagesSkipped: hostPackages.data ? null : hostPackages.reason ?? "unknown"
24944
+ };
24945
+ zip.file("bundle-truncated.json", JSON.stringify(marker, null, 2));
24946
+ }
24947
+ const zipBuffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
24948
+ const filename = `localazy-debug-bundle-${sessionShortId}-${formatTimestampForFilename(Date.now())}.zip`;
24949
+ return { zipBuffer, filename };
24950
+ };
24951
+ const DebugBundleController = ({ strapi: strapi2 }) => ({
24952
+ async getBundle(ctx) {
24953
+ const { sessionId } = ctx.params;
24954
+ const browserPayloadEncoded = typeof ctx.query.browser === "string" ? ctx.query.browser : null;
24955
+ try {
24956
+ const { zipBuffer, filename } = await buildDebugBundle({
24957
+ strapi: strapi2,
24958
+ sessionId,
24959
+ browserPayloadEncoded
24960
+ });
24961
+ ctx.type = "application/zip";
24962
+ ctx.set("Content-Disposition", `attachment; filename="${filename}"`);
24963
+ ctx.body = zipBuffer;
24964
+ } catch (err) {
24965
+ if (err instanceof SessionNotFoundError) {
24966
+ return ctx.notFound("Session not found");
24967
+ }
24968
+ strapi2.log.error(`[localazy] failed to build debug bundle: ${err instanceof Error ? err.message : err}`);
24969
+ throw err;
24970
+ }
24971
+ }
24972
+ });
24504
24973
  const controllers = {
24505
24974
  LocalazyUserController,
24506
24975
  LocalazyAuthController,
@@ -24509,7 +24978,8 @@ const controllers = {
24509
24978
  PluginSettingsController,
24510
24979
  LocalazyTransferController,
24511
24980
  EntryExclusionController,
24512
- ActivityLogsController
24981
+ ActivityLogsController,
24982
+ DebugBundleController
24513
24983
  };
24514
24984
  const verifyWebhook = (config2, { strapi: strapi2 }) => {
24515
24985
  return async (ctx, next) => {
@@ -24567,13 +25037,17 @@ const middlewares = {
24567
25037
  };
24568
25038
  const policies = {};
24569
25039
  const ROUTE_PREFIX$8 = "/user";
25040
+ const readPolicy$6 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25041
+ const settingsUpdatePolicy$4 = [
25042
+ { name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.SETTINGS_UPDATE] } }
25043
+ ];
24570
25044
  const LocalazyUserRoutes = [
24571
25045
  {
24572
25046
  method: "GET",
24573
25047
  path: `${ROUTE_PREFIX$8}`,
24574
25048
  handler: "LocalazyUserController.getUser",
24575
25049
  config: {
24576
- policies: []
25050
+ policies: readPolicy$6
24577
25051
  }
24578
25052
  },
24579
25053
  {
@@ -24581,7 +25055,7 @@ const LocalazyUserRoutes = [
24581
25055
  path: `${ROUTE_PREFIX$8}`,
24582
25056
  handler: "LocalazyUserController.updateUser",
24583
25057
  config: {
24584
- policies: []
25058
+ policies: settingsUpdatePolicy$4
24585
25059
  }
24586
25060
  },
24587
25061
  {
@@ -24589,18 +25063,21 @@ const LocalazyUserRoutes = [
24589
25063
  path: `${ROUTE_PREFIX$8}`,
24590
25064
  handler: "LocalazyUserController.deleteUser",
24591
25065
  config: {
24592
- policies: []
25066
+ policies: settingsUpdatePolicy$4
24593
25067
  }
24594
25068
  }
24595
25069
  ];
24596
25070
  const ROUTE_PREFIX$7 = "/auth";
25071
+ const settingsUpdatePolicy$3 = [
25072
+ { name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.SETTINGS_UPDATE] } }
25073
+ ];
24597
25074
  const LocalazyAuthRoutes = [
24598
25075
  {
24599
25076
  method: "GET",
24600
25077
  path: `${ROUTE_PREFIX$7}/generate-keys`,
24601
25078
  handler: "LocalazyAuthController.generateKeys",
24602
25079
  config: {
24603
- policies: []
25080
+ policies: settingsUpdatePolicy$3
24604
25081
  }
24605
25082
  },
24606
25083
  {
@@ -24608,18 +25085,26 @@ const LocalazyAuthRoutes = [
24608
25085
  path: `${ROUTE_PREFIX$7}/continuous-poll`,
24609
25086
  handler: "LocalazyAuthController.continuousPoll",
24610
25087
  config: {
24611
- policies: []
25088
+ policies: settingsUpdatePolicy$3
24612
25089
  }
24613
25090
  }
24614
25091
  ];
24615
25092
  const ROUTE_PREFIX$6 = "/plugin-settings";
25093
+ const readPolicy$5 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25094
+ const settingsUpdatePolicy$2 = [
25095
+ { name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.SETTINGS_UPDATE] } }
25096
+ ];
24616
25097
  const PluginSettingsRoutes = [
24617
25098
  {
24618
25099
  method: "GET",
24619
25100
  path: `${ROUTE_PREFIX$6}/content-transfer-setup`,
24620
25101
  handler: "PluginSettingsController.getContentTransferSetup",
24621
25102
  config: {
24622
- policies: []
25103
+ // Upload/Download pages call this to detect "model changed" before
25104
+ // transferring. Gating on `settings.read` would lock transfer-only
25105
+ // roles out of the transfer pages, so the read is open to any plugin
25106
+ // user. The destructive PUT below stays on `settings.update`.
25107
+ policies: readPolicy$5
24623
25108
  }
24624
25109
  },
24625
25110
  {
@@ -24627,7 +25112,7 @@ const PluginSettingsRoutes = [
24627
25112
  path: `${ROUTE_PREFIX$6}/content-transfer-setup`,
24628
25113
  handler: "PluginSettingsController.updateContentTransferSetup",
24629
25114
  config: {
24630
- policies: []
25115
+ policies: settingsUpdatePolicy$2
24631
25116
  }
24632
25117
  },
24633
25118
  {
@@ -24635,7 +25120,7 @@ const PluginSettingsRoutes = [
24635
25120
  path: `${ROUTE_PREFIX$6}/plugin-settings`,
24636
25121
  handler: "PluginSettingsController.getPluginSettings",
24637
25122
  config: {
24638
- policies: []
25123
+ policies: readPolicy$5
24639
25124
  }
24640
25125
  },
24641
25126
  {
@@ -24643,7 +25128,20 @@ const PluginSettingsRoutes = [
24643
25128
  path: `${ROUTE_PREFIX$6}/plugin-settings`,
24644
25129
  handler: "PluginSettingsController.updatePluginSettings",
24645
25130
  config: {
24646
- policies: []
25131
+ // Persists destructive Global Settings (webhookConfig, download.*, upload.*).
25132
+ // UI-pref-only writes use `PUT /plugin-settings/ui-prefs` (read-gated) below.
25133
+ policies: settingsUpdatePolicy$2
25134
+ }
25135
+ },
25136
+ {
25137
+ method: "PUT",
25138
+ path: `${ROUTE_PREFIX$6}/ui-prefs`,
25139
+ handler: "PluginSettingsController.updatePluginSettingsUiPrefs",
25140
+ config: {
25141
+ // Per-user UI state (last visited route, sort prefs). Service-side filters
25142
+ // the body to a fixed allowlist so a `read`-only caller cannot reach the
25143
+ // destructive fields that share the same plugin-settings store.
25144
+ policies: readPolicy$5
24647
25145
  }
24648
25146
  },
24649
25147
  {
@@ -24651,18 +25149,22 @@ const PluginSettingsRoutes = [
24651
25149
  path: `${ROUTE_PREFIX$6}/sync-cursor`,
24652
25150
  handler: "PluginSettingsController.getSyncCursor",
24653
25151
  config: {
24654
- policies: []
25152
+ policies: readPolicy$5
24655
25153
  }
24656
25154
  }
24657
25155
  ];
24658
25156
  const ROUTE_PREFIX$5 = "/strapi";
25157
+ const readPolicy$4 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25158
+ const settingsUpdatePolicy$1 = [
25159
+ { name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.SETTINGS_UPDATE] } }
25160
+ ];
24659
25161
  const StrapiRoutes = [
24660
25162
  {
24661
25163
  method: "GET",
24662
25164
  path: `${ROUTE_PREFIX$5}/models`,
24663
25165
  handler: "StrapiController.getModels",
24664
25166
  config: {
24665
- policies: []
25167
+ policies: readPolicy$4
24666
25168
  }
24667
25169
  },
24668
25170
  {
@@ -24670,7 +25172,7 @@ const StrapiRoutes = [
24670
25172
  path: `${ROUTE_PREFIX$5}/localizable-models`,
24671
25173
  handler: "StrapiController.getLocalizableModels",
24672
25174
  config: {
24673
- policies: []
25175
+ policies: readPolicy$4
24674
25176
  }
24675
25177
  },
24676
25178
  {
@@ -24678,7 +25180,7 @@ const StrapiRoutes = [
24678
25180
  path: `${ROUTE_PREFIX$5}/lifecycle/localazy-webhooks`,
24679
25181
  handler: "StrapiController.postLifecycleLocalazyWebhooks",
24680
25182
  config: {
24681
- policies: []
25183
+ policies: settingsUpdatePolicy$1
24682
25184
  }
24683
25185
  },
24684
25186
  {
@@ -24686,18 +25188,19 @@ const StrapiRoutes = [
24686
25188
  path: `${ROUTE_PREFIX$5}/version`,
24687
25189
  handler: "StrapiController.getPluginVersion",
24688
25190
  config: {
24689
- policies: []
25191
+ policies: readPolicy$4
24690
25192
  }
24691
25193
  }
24692
25194
  ];
24693
25195
  const ROUTE_PREFIX$4 = "/transfer";
25196
+ const transferPolicy$2 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.TRANSFER] } }];
24694
25197
  const LocalazyTransferRoutes = [
24695
25198
  {
24696
25199
  method: "POST",
24697
25200
  path: `${ROUTE_PREFIX$4}/upload`,
24698
25201
  handler: "LocalazyTransferController.upload",
24699
25202
  config: {
24700
- policies: []
25203
+ policies: transferPolicy$2
24701
25204
  }
24702
25205
  },
24703
25206
  {
@@ -24705,18 +25208,22 @@ const LocalazyTransferRoutes = [
24705
25208
  path: `${ROUTE_PREFIX$4}/download`,
24706
25209
  handler: "LocalazyTransferController.download",
24707
25210
  config: {
24708
- policies: []
25211
+ policies: transferPolicy$2
24709
25212
  }
24710
25213
  }
24711
25214
  ];
24712
25215
  const ROUTE_PREFIX$3 = "/project";
25216
+ const readPolicy$3 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25217
+ const settingsUpdatePolicy = [
25218
+ { name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.SETTINGS_UPDATE] } }
25219
+ ];
24713
25220
  const LocalazyProjectRoutes = [
24714
25221
  {
24715
25222
  method: "GET",
24716
25223
  path: `${ROUTE_PREFIX$3}`,
24717
25224
  handler: "LocalazyProjectController.getConnectedProject",
24718
25225
  config: {
24719
- policies: []
25226
+ policies: readPolicy$3
24720
25227
  }
24721
25228
  },
24722
25229
  {
@@ -24724,7 +25231,7 @@ const LocalazyProjectRoutes = [
24724
25231
  path: `${ROUTE_PREFIX$3}/webhooks`,
24725
25232
  handler: "LocalazyProjectController.getWebhookStatus",
24726
25233
  config: {
24727
- policies: []
25234
+ policies: readPolicy$3
24728
25235
  }
24729
25236
  },
24730
25237
  {
@@ -24732,7 +25239,7 @@ const LocalazyProjectRoutes = [
24732
25239
  path: `${ROUTE_PREFIX$3}/webhooks/setup`,
24733
25240
  handler: "LocalazyProjectController.setupWebhook",
24734
25241
  config: {
24735
- policies: []
25242
+ policies: settingsUpdatePolicy
24736
25243
  }
24737
25244
  },
24738
25245
  {
@@ -24740,7 +25247,7 @@ const LocalazyProjectRoutes = [
24740
25247
  path: `${ROUTE_PREFIX$3}/strapi-url`,
24741
25248
  handler: "LocalazyProjectController.getStrapiUrl",
24742
25249
  config: {
24743
- policies: []
25250
+ policies: readPolicy$3
24744
25251
  }
24745
25252
  }
24746
25253
  ];
@@ -24759,13 +25266,15 @@ const LocalazyPublicTransferRoutes = [
24759
25266
  }
24760
25267
  ];
24761
25268
  const ROUTE_PREFIX$1 = "/entry-exclusion";
25269
+ const readPolicy$2 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25270
+ const transferPolicy$1 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.TRANSFER] } }];
24762
25271
  const EntryExclusionRoutes = [
24763
25272
  {
24764
25273
  method: "GET",
24765
25274
  path: `${ROUTE_PREFIX$1}/:contentType/:documentId`,
24766
25275
  handler: "EntryExclusionController.getEntryExclusion",
24767
25276
  config: {
24768
- policies: []
25277
+ policies: readPolicy$2
24769
25278
  }
24770
25279
  },
24771
25280
  {
@@ -24773,7 +25282,7 @@ const EntryExclusionRoutes = [
24773
25282
  path: `${ROUTE_PREFIX$1}/:contentType/:documentId`,
24774
25283
  handler: "EntryExclusionController.setEntryExclusion",
24775
25284
  config: {
24776
- policies: []
25285
+ policies: transferPolicy$1
24777
25286
  }
24778
25287
  },
24779
25288
  {
@@ -24781,18 +25290,20 @@ const EntryExclusionRoutes = [
24781
25290
  path: `${ROUTE_PREFIX$1}/:contentType`,
24782
25291
  handler: "EntryExclusionController.getContentTypeExclusions",
24783
25292
  config: {
24784
- policies: []
25293
+ policies: readPolicy$2
24785
25294
  }
24786
25295
  }
24787
25296
  ];
24788
25297
  const ROUTE_PREFIX = "/activity-logs";
25298
+ const readPolicy$1 = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25299
+ const transferPolicy = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.TRANSFER] } }];
24789
25300
  const ActivityLogsRoutes = [
24790
25301
  {
24791
25302
  method: "GET",
24792
25303
  path: `${ROUTE_PREFIX}`,
24793
25304
  handler: "ActivityLogsController.getSessions",
24794
25305
  config: {
24795
- policies: []
25306
+ policies: readPolicy$1
24796
25307
  }
24797
25308
  },
24798
25309
  {
@@ -24800,7 +25311,7 @@ const ActivityLogsRoutes = [
24800
25311
  path: `${ROUTE_PREFIX}/export`,
24801
25312
  handler: "ActivityLogsController.exportSessions",
24802
25313
  config: {
24803
- policies: []
25314
+ policies: readPolicy$1
24804
25315
  }
24805
25316
  },
24806
25317
  {
@@ -24808,7 +25319,7 @@ const ActivityLogsRoutes = [
24808
25319
  path: `${ROUTE_PREFIX}/:sessionId`,
24809
25320
  handler: "ActivityLogsController.getSession",
24810
25321
  config: {
24811
- policies: []
25322
+ policies: readPolicy$1
24812
25323
  }
24813
25324
  },
24814
25325
  {
@@ -24816,13 +25327,28 @@ const ActivityLogsRoutes = [
24816
25327
  path: `${ROUTE_PREFIX}`,
24817
25328
  handler: "ActivityLogsController.clearSessions",
24818
25329
  config: {
24819
- policies: []
25330
+ policies: transferPolicy
24820
25331
  }
24821
25332
  }
24822
25333
  ];
25334
+ const readPolicy = [{ name: "admin::hasPermissions", config: { actions: [PERMISSION_UIDS.READ] } }];
25335
+ const DebugBundleRoutes = [
25336
+ {
25337
+ method: "GET",
25338
+ path: "/activity-logs/:sessionId/bundle",
25339
+ handler: "DebugBundleController.getBundle",
25340
+ config: {
25341
+ policies: readPolicy
25342
+ }
25343
+ }
25344
+ ];
25345
+ const withoutAdminPolicies = (routes2) => routes2.map((route) => ({
25346
+ ...route,
25347
+ config: { ...route.config, policies: [] }
25348
+ }));
24823
25349
  const routes = {
24824
25350
  "content-api": {
24825
- routes: [...StrapiRoutes]
25351
+ routes: withoutAdminPolicies(StrapiRoutes)
24826
25352
  },
24827
25353
  admin: {
24828
25354
  type: "admin",
@@ -24834,7 +25360,8 @@ const routes = {
24834
25360
  ...LocalazyTransferRoutes,
24835
25361
  ...LocalazyProjectRoutes,
24836
25362
  ...EntryExclusionRoutes,
24837
- ...ActivityLogsRoutes
25363
+ ...ActivityLogsRoutes,
25364
+ ...DebugBundleRoutes
24838
25365
  ]
24839
25366
  },
24840
25367
  LocalazyPublicTransferRoutes: {
@@ -25033,6 +25560,7 @@ const KEY$1 = "sync-cursor";
25033
25560
  const emptySyncCursor = {
25034
25561
  processedKeys: {}
25035
25562
  };
25563
+ const UI_PREF_FIELDS = ["defaultRoute", "activityLogsSort"];
25036
25564
  const PluginSettingsService = ({ strapi: strapi2 }) => ({
25037
25565
  async getContentTransferSetup() {
25038
25566
  const pluginStore = getStrapiStore(strapi2);
@@ -25069,6 +25597,20 @@ const PluginSettingsService = ({ strapi: strapi2 }) => ({
25069
25597
  });
25070
25598
  return newSettings;
25071
25599
  },
25600
+ // Counterpart to `updatePluginSettings` for the `read`-gated UI-prefs route.
25601
+ // Drops any field outside `UI_PREF_FIELDS` server-side so a `read`-only caller
25602
+ // cannot reach `webhookConfig` / `download.*` / `upload.*` via the merge.
25603
+ async updatePluginSettingsUiPrefs(prefs) {
25604
+ const sanitized = {};
25605
+ if (prefs && typeof prefs === "object") {
25606
+ for (const field of UI_PREF_FIELDS) {
25607
+ if (field in prefs) {
25608
+ sanitized[field] = prefs[field];
25609
+ }
25610
+ }
25611
+ }
25612
+ return this.updatePluginSettings(sanitized);
25613
+ },
25072
25614
  async getSyncCursor() {
25073
25615
  const pluginStore = getStrapiStore(strapi2);
25074
25616
  const cursor = await pluginStore.get({ key: KEY$1 });
@@ -25113,6 +25655,8 @@ const LocalazyTransferUploadService = ({ strapi: strapi2 }) => ({
25113
25655
  const { setup: setup2 } = contentTransferSetup;
25114
25656
  const collectionsNames = getCollectionsNames(setup2);
25115
25657
  const models = await StrapiService2.getModels();
25658
+ const strapiDefaultLocaleCode = await StrapiI18nService2.getDefaultLocaleCode();
25659
+ const attemptedTuples = [];
25116
25660
  let pickedFlattenStrapiContent = {};
25117
25661
  for (const collectionName of collectionsNames) {
25118
25662
  const currentModel = models.find((model) => model.collectionName === collectionName);
@@ -25175,6 +25719,15 @@ const LocalazyTransferUploadService = ({ strapi: strapi2 }) => ({
25175
25719
  });
25176
25720
  await activityLogsService.addEntry(sessionId, message);
25177
25721
  strapi2.log.info(message);
25722
+ if (strapiDefaultLocaleCode) {
25723
+ for (const entry of entries) {
25724
+ attemptedTuples.push({
25725
+ uid: modelUid,
25726
+ documentId: entry.documentId,
25727
+ locale: strapiDefaultLocaleCode
25728
+ });
25729
+ }
25730
+ }
25178
25731
  entries = omitDeep(entries, ["locale", "createdAt", "updatedAt", "publishedAt"]);
25179
25732
  entries.forEach((entry) => {
25180
25733
  const flatten = flattenObject({
@@ -25192,7 +25745,6 @@ const LocalazyTransferUploadService = ({ strapi: strapi2 }) => ({
25192
25745
  ...characterLimitsMetadata
25193
25746
  };
25194
25747
  }
25195
- const strapiDefaultLocaleCode = await StrapiI18nService2.getDefaultLocaleCode();
25196
25748
  const localazyLocaleCode = strapiDefaultLocaleCode ? isoStrapiToLocalazy(strapiDefaultLocaleCode) : config.default.LOCALAZY_DEFAULT_LOCALE;
25197
25749
  const importFile = LocalazyUploadService2.createImportFileRepresentation(
25198
25750
  localazyLocaleCode,
@@ -25214,6 +25766,10 @@ const LocalazyTransferUploadService = ({ strapi: strapi2 }) => ({
25214
25766
  await activityLogsService.addEntry(sessionId, "Uploading collections to Localazy...");
25215
25767
  try {
25216
25768
  await LocalazyUploadService2.upload(importFile, uploadConfig);
25769
+ await activityLogsService.recordAttemptedEntries(
25770
+ sessionId,
25771
+ attemptedTuples.map((t3) => ({ ...t3, status: "success" }))
25772
+ );
25217
25773
  } catch (error) {
25218
25774
  success = false;
25219
25775
  const errorMessage = `Upload to Localazy failed: ${error instanceof Error ? error.message : String(error)}`;
@@ -25223,6 +25779,10 @@ const LocalazyTransferUploadService = ({ strapi: strapi2 }) => ({
25223
25779
  message: errorMessage
25224
25780
  });
25225
25781
  await activityLogsService.addEntry(sessionId, errorMessage);
25782
+ await activityLogsService.recordAttemptedEntries(
25783
+ sessionId,
25784
+ attemptedTuples.map((t3) => ({ ...t3, status: "failed", errorMessage }))
25785
+ );
25226
25786
  await activityLogsService.finishSession(sessionId, "failed", errorMessage);
25227
25787
  console.timeEnd("upload");
25228
25788
  return;
@@ -25627,7 +26187,7 @@ const shouldSetDownloadedProperty = (modelContentTransferSetup, parsedKeyRest) =
25627
26187
  return "no";
25628
26188
  }
25629
26189
  };
25630
- const getFilteredLanguagesCodesForDownload = async (languagesCodes) => {
26190
+ const getFilteredLanguagesCodesForDownload = async (languagesCodes, requestedUiLanguages) => {
25631
26191
  const pluginSettingsServiceHelper = new PluginSettingsServiceHelper();
25632
26192
  const requestInitiatorHelper = new RequestInitiatorHelper(strapi);
25633
26193
  await pluginSettingsServiceHelper.setup();
@@ -25635,7 +26195,7 @@ const getFilteredLanguagesCodesForDownload = async (languagesCodes) => {
25635
26195
  if (requestInitiatorHelper.isInitiatedByLocalazyWebhook()) {
25636
26196
  localLanguagesCodes = pluginSettingsServiceHelper.getWebhookLanguagesCodes();
25637
26197
  } else if (requestInitiatorHelper.isInitiatedByLocalazyPluginUI()) {
25638
- localLanguagesCodes = pluginSettingsServiceHelper.getUiLanguagesCodes();
26198
+ localLanguagesCodes = requestedUiLanguages ?? pluginSettingsServiceHelper.getUiLanguagesCodes();
25639
26199
  } else {
25640
26200
  strapi.log.warn("Called by an unknown initiator.");
25641
26201
  return languagesCodes;
@@ -25648,7 +26208,8 @@ const getFilteredLanguagesCodesForDownload = async (languagesCodes) => {
25648
26208
  const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25649
26209
  async download({
25650
26210
  notificationService,
25651
- fullSync = false
26211
+ fullSync = false,
26212
+ requestedUiLanguages
25652
26213
  }) {
25653
26214
  console.time("download");
25654
26215
  const syncMode = fullSync ? "Full sync" : "Incremental sync";
@@ -25728,7 +26289,7 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25728
26289
  return;
25729
26290
  }
25730
26291
  let languagesCodes = projectLanguagesWithoutSourceLanguage.map((language3) => language3.code);
25731
- languagesCodes = await getFilteredLanguagesCodesForDownload(languagesCodes);
26292
+ languagesCodes = await getFilteredLanguagesCodesForDownload(languagesCodes, requestedUiLanguages);
25732
26293
  const strapiUnsupportedLanguages = [];
25733
26294
  const strapiLocales = await StrapiI18nService2.getLocales();
25734
26295
  for (const isoLocalazy of languagesCodes) {
@@ -25947,6 +26508,12 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25947
26508
  message
25948
26509
  });
25949
26510
  await activityLogsService.addEntry(sessionId, message);
26511
+ await activityLogsService.recordAttemptedEntry(sessionId, {
26512
+ uid,
26513
+ documentId,
26514
+ locale: isoStrapi,
26515
+ status: "success"
26516
+ });
25950
26517
  await markKeysProcessed(sourceKeys);
25951
26518
  } catch (e2) {
25952
26519
  success = false;
@@ -25963,6 +26530,13 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25963
26530
  sessionId,
25964
26531
  `Failed to create ${uid} [${documentId}] in ${isoStrapi}: ${e2.message}`
25965
26532
  );
26533
+ await activityLogsService.recordAttemptedEntry(sessionId, {
26534
+ uid,
26535
+ documentId,
26536
+ locale: isoStrapi,
26537
+ status: "failed",
26538
+ errorMessage: e2.message
26539
+ });
25966
26540
  }
25967
26541
  } else {
25968
26542
  try {
@@ -25981,6 +26555,12 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25981
26555
  message
25982
26556
  });
25983
26557
  await activityLogsService.addEntry(sessionId, message);
26558
+ await activityLogsService.recordAttemptedEntry(sessionId, {
26559
+ uid,
26560
+ documentId,
26561
+ locale: isoStrapi,
26562
+ status: "success"
26563
+ });
25984
26564
  await markKeysProcessed(sourceKeys);
25985
26565
  } catch (e2) {
25986
26566
  success = false;
@@ -25997,6 +26577,13 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
25997
26577
  sessionId,
25998
26578
  `Failed to update ${uid} [${documentId}] in ${isoStrapi}: ${e2.message}`
25999
26579
  );
26580
+ await activityLogsService.recordAttemptedEntry(sessionId, {
26581
+ uid,
26582
+ documentId,
26583
+ locale: isoStrapi,
26584
+ status: "failed",
26585
+ errorMessage: e2.message
26586
+ });
26000
26587
  }
26001
26588
  }
26002
26589
  } catch (e2) {
@@ -26009,6 +26596,13 @@ const LocalazyTransferDownloadService = ({ strapi: strapi2 }) => ({
26009
26596
  sessionId,
26010
26597
  `Error processing ${uid} [${documentId}] in ${isoStrapi}: ${e2.message}`
26011
26598
  );
26599
+ await activityLogsService.recordAttemptedEntry(sessionId, {
26600
+ uid,
26601
+ documentId,
26602
+ locale: isoStrapi,
26603
+ status: "failed",
26604
+ errorMessage: e2.message
26605
+ });
26012
26606
  }
26013
26607
  }
26014
26608
  }
@@ -26759,6 +27353,7 @@ const emptyActivityLogs = {
26759
27353
  sessions: []
26760
27354
  };
26761
27355
  const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
27356
+ const ATTEMPTED_ENTRIES_CAP = 5e3;
26762
27357
  let writeLock = Promise.resolve();
26763
27358
  const withLock = async (fn) => {
26764
27359
  let release;
@@ -26823,6 +27418,38 @@ const ActivityLogsService = ({ strapi: strapi2 }) => ({
26823
27418
  await this.saveActivityLogs(logs);
26824
27419
  });
26825
27420
  },
27421
+ async recordAttemptedEntry(sessionId, entry) {
27422
+ return this.recordAttemptedEntries(sessionId, [entry]);
27423
+ },
27424
+ async recordAttemptedEntries(sessionId, entries) {
27425
+ if (!entries.length) {
27426
+ return;
27427
+ }
27428
+ return withLock(async () => {
27429
+ const logs = await this.getActivityLogs();
27430
+ const session = logs.sessions.find((s2) => s2.id === sessionId);
27431
+ if (!session) {
27432
+ return;
27433
+ }
27434
+ if (!session.attemptedEntries) {
27435
+ session.attemptedEntries = [];
27436
+ }
27437
+ const now = Date.now();
27438
+ const wasAtCap = session.attemptedEntries.length >= ATTEMPTED_ENTRIES_CAP;
27439
+ const remaining = Math.max(0, ATTEMPTED_ENTRIES_CAP - session.attemptedEntries.length);
27440
+ const accepted = entries.slice(0, remaining);
27441
+ const dropped = entries.length - accepted.length;
27442
+ for (const entry of accepted) {
27443
+ session.attemptedEntries.push({ ...entry, attemptedAt: now });
27444
+ }
27445
+ if (dropped > 0 && !wasAtCap) {
27446
+ strapi2.log.warn(
27447
+ `[localazy] attemptedEntries cap (${ATTEMPTED_ENTRIES_CAP}) reached for session ${sessionId}; ${dropped} record(s) dropped.`
27448
+ );
27449
+ }
27450
+ await this.saveActivityLogs(logs);
27451
+ });
27452
+ },
26826
27453
  async finishSession(sessionId, status, summary) {
26827
27454
  return withLock(async () => {
26828
27455
  const logs = await this.getActivityLogs();