@antelopejs/dms 0.7.13 → 0.7.15

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 (85) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/db/models/userNotifications.model.js +3 -27
  3. package/dist/db/models/userNotifications.model.js.map +1 -1
  4. package/dist/implementations/dms/client-ip.d.ts +2 -0
  5. package/dist/implementations/dms/client-ip.js +9 -0
  6. package/dist/implementations/dms/client-ip.js.map +1 -0
  7. package/dist/implementations/dms/frontend-modules.d.ts +4 -2
  8. package/dist/implementations/dms/frontend-modules.js +4 -0
  9. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  10. package/dist/implementations/dms/frontend-public-endpoints.d.ts +3 -0
  11. package/dist/implementations/dms/frontend-public-endpoints.js +70 -0
  12. package/dist/implementations/dms/frontend-public-endpoints.js.map +1 -0
  13. package/dist/implementations/dms/manifest.d.ts +1 -0
  14. package/dist/implementations/dms/manifest.js +3 -1
  15. package/dist/implementations/dms/manifest.js.map +1 -1
  16. package/dist/index.js +1 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/routes/auth/client-ip.d.ts +1 -4
  19. package/dist/routes/auth/client-ip.js.map +1 -1
  20. package/dist/test/integration/notifications.test.js +6 -3
  21. package/dist/test/integration/notifications.test.js.map +1 -1
  22. package/dist/test/unit/implementations/dms/frontend-public-endpoints.test.d.ts +1 -0
  23. package/dist/test/unit/implementations/dms/frontend-public-endpoints.test.js +74 -0
  24. package/dist/test/unit/implementations/dms/frontend-public-endpoints.test.js.map +1 -0
  25. package/dist/test/unit/implementations/dms/manifest-strip.test.js +5 -0
  26. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  27. package/dist/test/unit/interfaces/dms/client-ip.test.d.ts +1 -0
  28. package/dist/test/unit/interfaces/dms/client-ip.test.js +61 -0
  29. package/dist/test/unit/interfaces/dms/client-ip.test.js.map +1 -0
  30. package/frontend-vue/layers/dms-core/app/composables/period/periodLink.ts +60 -0
  31. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +9 -4
  32. package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +34 -4
  33. package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +2 -0
  34. package/frontend-vue/layers/dms-layout/app/build/utils/pageValidationRedirect.ts +32 -0
  35. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +35 -12
  36. package/frontend-vue/layers/dms-layout/app/emails/components/email-logo.ts +54 -0
  37. package/frontend-vue/layers/dms-layout/app/emails/components/index.ts +2 -0
  38. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +29 -7
  39. package/frontend-vue/layers/dms-layout/app/types/page.ts +2 -0
  40. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +39 -2
  41. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +18 -1
  42. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +28 -7
  43. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmTextMatch.ts +16 -0
  44. package/frontend-vue/layers/dms-ui/app/build/composables/form/formControlErrors.ts +10 -0
  45. package/frontend-vue/layers/dms-ui/app/build/composables/form/numberRange.ts +53 -0
  46. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +8 -32
  47. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +6 -29
  48. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +34 -3
  49. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/urlFilters.ts +29 -0
  50. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +6 -3
  51. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +12 -2
  52. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +2 -2
  53. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +1 -1
  54. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +1 -1
  55. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +47 -8
  56. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTags.vue +1 -1
  57. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +1 -1
  58. package/frontend-vue/layers/dms-ui/app/components/form/components/KeyValue.vue +1 -1
  59. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +61 -2
  60. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +21 -1
  61. package/frontend-vue/layers/dms-ui/app/composables/confirm/index.ts +1 -0
  62. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +20 -0
  63. package/frontend-vue/layers/dms-ui/app/composables/confirm/useConfirmFrom.ts +32 -0
  64. package/frontend-vue/layers/dms-ui/app/composables/form/useControlError.ts +30 -0
  65. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +2 -2
  66. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  67. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +6 -0
  68. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +6 -0
  69. package/frontend-vue/tests/confirm-from.test.ts +101 -0
  70. package/frontend-vue/tests/confirm-typed-text.test.ts +35 -0
  71. package/frontend-vue/tests/context-links.test.ts +220 -0
  72. package/frontend-vue/tests/control-error-path.test.ts +11 -0
  73. package/frontend-vue/tests/email-layout.test.ts +124 -0
  74. package/frontend-vue/tests/field-errors.test.ts +35 -0
  75. package/frontend-vue/tests/form-required-legend.test.ts +34 -0
  76. package/frontend-vue/tests/form-server-field-errors.test.ts +18 -1
  77. package/frontend-vue/tests/input-number-range.test.ts +135 -0
  78. package/frontend-vue/tests/input-tags.test.ts +1 -1
  79. package/frontend-vue/tests/page-validation-redirect.test.ts +221 -0
  80. package/frontend-vue/tests/row-side-panel.test.ts +2 -2
  81. package/frontend-vue/tests/table-url-filters.test.ts +158 -0
  82. package/frontend-vue/tests/table-view-bulk-drawer.test.ts +23 -0
  83. package/package.json +2 -2
  84. package/skills/dms-module/SKILL.md +3 -0
  85. /package/frontend-vue/layers/dms-ui/app/{build/components → components}/form/FormRequiredLegend.vue +0 -0
@@ -0,0 +1,221 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ computed,
4
+ createSSRApp,
5
+ h,
6
+ inject,
7
+ onMounted,
8
+ onUnmounted,
9
+ provide,
10
+ ref,
11
+ toRef,
12
+ watch,
13
+ type FunctionalComponent,
14
+ } from "vue";
15
+ import { renderToString } from "vue/server-renderer";
16
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
17
+ import CatchAllPage from "../layers/dms-layout/app/pages/[...slug].vue";
18
+ import { pageValidationRedirect } from "../layers/dms-layout/app/build/utils/pageValidationRedirect";
19
+ import {
20
+ runCustomValidation,
21
+ validateRequiredQueryParams,
22
+ } from "../layers/dms-core/app/utils/query-validation";
23
+ import {
24
+ PAGE_FILL_HEIGHT_CLASSES,
25
+ providePageFillHeight,
26
+ usePageFillHeight,
27
+ } from "../layers/dms-layout/app/composables/layout/usePageFillHeight";
28
+ import type { PageValidation } from "../layers/dms-layout/app/types/page";
29
+
30
+ vi.mock(
31
+ "../layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder",
32
+ async () => {
33
+ const { ref } = await import("vue");
34
+ return { useDevReloading: () => ref(false) };
35
+ },
36
+ );
37
+
38
+ const PAGE_PATH = "/modules/marketing/funnel";
39
+ const LIST_PATH = "/modules/marketing/funnels";
40
+ const NOT_FOUND = 404;
41
+
42
+ class HttpError extends Error {
43
+ constructor(readonly statusCode: number) {
44
+ super(`HTTP ${statusCode}`);
45
+ }
46
+ }
47
+
48
+ const navigateDms = vi.fn(async () => {});
49
+ const pageQuery = ref<Record<string, unknown>>({});
50
+ const validation = ref<PageValidation | undefined>(undefined);
51
+
52
+ const PageComponent: FunctionalComponent<{ componentId: string }> = (props) =>
53
+ h("section", { "data-component": props.componentId });
54
+ PageComponent.props = ["componentId"];
55
+
56
+ function installRuntime(): void {
57
+ const pageLayout = ref({
58
+ components: { report: { componentName: "FunnelReport" } },
59
+ });
60
+ const runtime: Record<string, unknown> = {
61
+ computed,
62
+ inject,
63
+ onMounted,
64
+ onUnmounted,
65
+ provide,
66
+ ref,
67
+ toRef,
68
+ watch,
69
+ HTTP_FORBIDDEN: 403,
70
+ HTTP_INTERNAL_SERVER_ERROR: 500,
71
+ HTTP_NOT_FOUND: NOT_FOUND,
72
+ PAGE_FILL_HEIGHT_CLASSES,
73
+ providePageFillHeight,
74
+ usePageFillHeight,
75
+ createError: ({ statusCode }: { statusCode: number }) =>
76
+ new HttpError(statusCode),
77
+ defineDmsPageMeta: vi.fn(),
78
+ firstAccessiblePagePath: vi.fn(),
79
+ navigateDms,
80
+ preloadPageLayoutComponents: vi.fn(),
81
+ providePageRealtime: vi.fn(),
82
+ resolveDmsComponent: () => "div",
83
+ runCustomValidation,
84
+ showError: vi.fn(),
85
+ validateRequiredQueryParams,
86
+ useDefinedFunctions: () => ({ getFunction: () => undefined }),
87
+ useDmsRoute: () => ({ path: PAGE_PATH, query: pageQuery.value }),
88
+ useHomepage: () => "/",
89
+ useI18n: () => ({ t: (key: string) => key }),
90
+ usePageLayout: async () => ({ pageLayout }),
91
+ usePageRealtime: () => ({}),
92
+ usePermissions: () => ({
93
+ permissions: ref([]),
94
+ refreshIfStale: async () => {},
95
+ }),
96
+ useSessionRecovery: () => ({
97
+ loggedIn: ref(true),
98
+ reconcileSession: async () => {},
99
+ redirectToAuth: async () => {},
100
+ }),
101
+ useSiteLayout: () => ({
102
+ siteLayout: ref({ pages: {} }),
103
+ siteLayoutTree: ref(undefined),
104
+ loadingError: ref(null),
105
+ loadSiteLayout: async () => {},
106
+ refreshIfStale: async () => {},
107
+ findMatchingRoute: () => ({
108
+ metadata: {
109
+ fullId: "marketing.funnel",
110
+ hasAccess: true,
111
+ validation: validation.value,
112
+ },
113
+ params: {},
114
+ }),
115
+ }),
116
+ };
117
+ Object.entries(runtime).forEach(([name, value]) =>
118
+ vi.stubGlobal(name, value),
119
+ );
120
+ }
121
+
122
+ interface RenderedPage {
123
+ host: HTMLElement;
124
+ errors: unknown[];
125
+ }
126
+
127
+ async function renderPage(): Promise<RenderedPage> {
128
+ const errors: unknown[] = [];
129
+ const app = createSSRApp(() => h(CatchAllPage));
130
+ app.config.errorHandler = (error) => {
131
+ errors.push(error);
132
+ };
133
+ app.component("DmsRecursiveComponent", PageComponent);
134
+ const host = document.createElement("div");
135
+ host.innerHTML = await renderToString(app);
136
+ return { host, errors };
137
+ }
138
+
139
+ beforeEach(() => {
140
+ navigateDms.mockClear();
141
+ pageQuery.value = {};
142
+ validation.value = undefined;
143
+ installRuntime();
144
+ });
145
+
146
+ afterEach(() => {
147
+ vi.unstubAllGlobals();
148
+ });
149
+
150
+ describe("pageValidationRedirect", () => {
151
+ const context = {
152
+ routeParams: { id: "42" },
153
+ routeQuery: { website: "site-1" },
154
+ };
155
+
156
+ it("is undefined without a redirectTo", () => {
157
+ expect(
158
+ pageValidationRedirect(
159
+ { requiredQueryParams: ["id"] },
160
+ PAGE_PATH,
161
+ context,
162
+ ),
163
+ ).toBeUndefined();
164
+ });
165
+
166
+ it("fills the page URL's tokens and leaves out an empty query pair", () => {
167
+ expect(
168
+ pageValidationRedirect(
169
+ {
170
+ redirectTo: `${LIST_PATH}?website={{query.website}}&tab={{query.tab}}`,
171
+ },
172
+ PAGE_PATH,
173
+ context,
174
+ ),
175
+ ).toBe(`${LIST_PATH}?website=site-1`);
176
+ });
177
+
178
+ it("refuses a target on the page's own path, which would fail again", () => {
179
+ expect(
180
+ pageValidationRedirect(
181
+ { redirectTo: `${PAGE_PATH}/?id={{query.id}}` },
182
+ PAGE_PATH,
183
+ context,
184
+ ),
185
+ ).toBeUndefined();
186
+ expect(
187
+ pageValidationRedirect({ redirectTo: "?id=1" }, PAGE_PATH, context),
188
+ ).toBeUndefined();
189
+ });
190
+ });
191
+
192
+ describe("a page failing its validation", () => {
193
+ it("answers not found without a redirectTo", async () => {
194
+ validation.value = { requiredQueryParams: ["id"] };
195
+ const { errors } = await renderPage();
196
+ expect(errors).toEqual([new HttpError(NOT_FOUND)]);
197
+ expect(navigateDms).not.toHaveBeenCalled();
198
+ });
199
+
200
+ it("replaces the URL with its redirectTo, drawing none of its blocks", async () => {
201
+ validation.value = {
202
+ requiredQueryParams: ["id"],
203
+ redirectTo: `${LIST_PATH}?website={{query.website}}`,
204
+ };
205
+ pageQuery.value = { website: "site-1" };
206
+ const { host, errors } = await renderPage();
207
+ expect(errors).toEqual([]);
208
+ expect(navigateDms).toHaveBeenCalledWith(`${LIST_PATH}?website=site-1`, {
209
+ replace: true,
210
+ });
211
+ expect(host.querySelector("[data-component]")).toBeNull();
212
+ });
213
+
214
+ it("draws its blocks once the required query is there", async () => {
215
+ validation.value = { requiredQueryParams: ["id"], redirectTo: LIST_PATH };
216
+ pageQuery.value = { id: "f1" };
217
+ const { host } = await renderPage();
218
+ expect(navigateDms).not.toHaveBeenCalled();
219
+ expect(host.querySelector("[data-component]")).not.toBeNull();
220
+ });
221
+ });
@@ -453,13 +453,13 @@ describe("deep link to a row the table does not list", () => {
453
453
  ).toEqual({ isMissing: true });
454
454
  });
455
455
 
456
- it("finds nothing, missing or not, on a refusal or without a row route", async () => {
456
+ it("marks a refused read as failed and finds nothing without a row route", async () => {
457
457
  const refused = async () => {
458
458
  throw Object.assign(new Error("Forbidden"), { status: 403 });
459
459
  };
460
460
  expect(
461
461
  await resolveDeepLinkedRow("r9", { listed: rows, idOf, getRow: refused }),
462
- ).toEqual({ isMissing: false });
462
+ ).toEqual({ isMissing: false, isFailed: true });
463
463
  const noRoute = async () => undefined;
464
464
  expect(
465
465
  await resolveDeepLinkedRow("r9", { listed: rows, idOf, getRow: noRoute }),
@@ -0,0 +1,158 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ computed,
4
+ createApp,
5
+ defineComponent,
6
+ h,
7
+ nextTick,
8
+ provide,
9
+ ref,
10
+ shallowRef,
11
+ type App,
12
+ type FunctionalComponent,
13
+ } from "vue";
14
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
15
+ import TableFiltersRow from "../layers/dms-ui/app/build/components/table/FiltersRow.vue";
16
+ import type { TableUrlFilter } from "../layers/dms-ui/app/build/components/table/Table.vue";
17
+ import {
18
+ visibleUrlFilters,
19
+ withoutQueryParam,
20
+ } from "../layers/dms-ui/app/build/composables/table-view/utils/urlFilters";
21
+
22
+ describe("visibleUrlFilters", () => {
23
+ const declared = {
24
+ email: { field: "email", visible: true, label: "$app.email" },
25
+ template: { field: "templateSlug", mode: "is_not", visible: true },
26
+ tenant: { field: "tenantId" },
27
+ };
28
+
29
+ it("lists the visible filters the URL applies, hidden ones left out", () => {
30
+ expect(
31
+ visibleUrlFilters(declared, {
32
+ email: "a@b.test",
33
+ tenant: "t1",
34
+ page: "2",
35
+ }),
36
+ ).toEqual([
37
+ {
38
+ param: "email",
39
+ label: "$app.email",
40
+ accessorKey: "email",
41
+ mode: "is",
42
+ value: "a@b.test",
43
+ },
44
+ ]);
45
+ expect(visibleUrlFilters(declared, { template: "welcome" })).toEqual([
46
+ {
47
+ param: "template",
48
+ label: undefined,
49
+ accessorKey: "templateSlug",
50
+ mode: "is_not",
51
+ value: "welcome",
52
+ },
53
+ ]);
54
+ expect(visibleUrlFilters(undefined, { email: "a@b.test" })).toEqual([]);
55
+ });
56
+
57
+ it("removes one parameter from the query", () => {
58
+ expect(withoutQueryParam({ email: "a", tab: "all" }, "email")).toEqual({
59
+ tab: "all",
60
+ });
61
+ });
62
+ });
63
+
64
+ describe("filters row URL chips", () => {
65
+ let app: App | undefined;
66
+ const emits = vi.fn();
67
+
68
+ const mount = (urlFilters: TableUrlFilter[]) => {
69
+ const Host = defineComponent({
70
+ setup() {
71
+ provide(
72
+ "tableSharedData",
73
+ shallowRef({
74
+ columnFiltersState: ref([]),
75
+ labeledColumns: computed(() => [
76
+ {
77
+ column: { id: "templateSlug", columnDef: {} },
78
+ label: "Template",
79
+ },
80
+ ]),
81
+ clearableFilters: computed(() => ({
82
+ filters: false,
83
+ search: false,
84
+ })),
85
+ urlFilters,
86
+ emits,
87
+ }),
88
+ );
89
+ return () => h(TableFiltersRow);
90
+ },
91
+ });
92
+ app = createApp(Host);
93
+ const passthrough =
94
+ (tag: string): FunctionalComponent =>
95
+ (_props, { slots }) =>
96
+ h(tag, slots.default?.());
97
+ app.component("UPopover", passthrough("div"));
98
+ app.component("UButton", passthrough("button"));
99
+ app.component("UIcon", passthrough("i"));
100
+ app.component("Suspense", passthrough("div"));
101
+ const container = document.createElement("div");
102
+ app.mount(container);
103
+ return container;
104
+ };
105
+
106
+ beforeEach(() => {
107
+ emits.mockClear();
108
+ vi.stubGlobal("computed", computed);
109
+ vi.stubGlobal("ref", ref);
110
+ vi.stubGlobal("useI18n", () => ({
111
+ t: (key: string) => key,
112
+ locale: ref("en"),
113
+ }));
114
+ vi.stubGlobal("useTranslation", () => ({
115
+ processI18n: (text: string) =>
116
+ text.startsWith("$") ? `t:${text.slice(1)}` : text,
117
+ }));
118
+ vi.stubGlobal("useDmsAppConfig", () => ({
119
+ ui: { icons: { close: "i-close", plus: "i-plus" } },
120
+ }));
121
+ vi.stubGlobal("useDataTypes", () => ({ getDataType: () => undefined }));
122
+ vi.stubGlobal("isString", (value: unknown) => typeof value === "string");
123
+ vi.stubGlobal("resolveDmsComponent", () => undefined);
124
+ });
125
+
126
+ afterEach(() => {
127
+ app?.unmount();
128
+ app = undefined;
129
+ vi.unstubAllGlobals();
130
+ });
131
+
132
+ it("draws a chip per URL filter and asks to remove its parameter", async () => {
133
+ const container = mount([
134
+ {
135
+ param: "email",
136
+ label: "$app.email",
137
+ accessorKey: "email",
138
+ mode: "is",
139
+ value: "a@b.test",
140
+ },
141
+ {
142
+ param: "template",
143
+ accessorKey: "templateSlug",
144
+ mode: "is",
145
+ value: "welcome",
146
+ },
147
+ ]);
148
+ const chips = container.querySelectorAll("[data-dms-url-filter]");
149
+ expect(chips).toHaveLength(2);
150
+ expect(chips[0]!.textContent).toContain("t:app.email");
151
+ expect(chips[0]!.textContent).toContain("a@b.test");
152
+ expect(chips[1]!.textContent).toContain("Template");
153
+
154
+ chips[1]!.querySelector("button")!.click();
155
+ await nextTick();
156
+ expect(emits).toHaveBeenCalledWith("removeUrlFilter", "template");
157
+ });
158
+ });
@@ -13,6 +13,7 @@ import {
13
13
  rowNavigation,
14
14
  } from "../layers/dms-ui/app/build/composables/actions/rowNavigation";
15
15
  import {
16
+ deepLinkOutcome,
16
17
  readRecordId,
17
18
  recordUrlKey,
18
19
  writeRecordId,
@@ -130,6 +131,28 @@ describe("record deep links", () => {
130
131
  writeRecordId({ tableId: "runs", isSoleTableView: true });
131
132
  expect(window.location.search).toBe("?tab=open");
132
133
  });
134
+
135
+ it("notifies a link no row answers, unless a side panel shows it missing", () => {
136
+ const panel = {
137
+ target: { type: "sidePanel" },
138
+ } as unknown as CustomRowAction;
139
+ const drawer = {
140
+ target: { type: "drawer" },
141
+ deepLink: true,
142
+ } as unknown as CustomRowAction;
143
+ const row = { id: "r1" };
144
+ expect(deepLinkOutcome({ row, isMissing: false }, drawer)).toBe("open");
145
+ expect(deepLinkOutcome({ isMissing: true }, panel)).toBe("missingPanel");
146
+ expect(deepLinkOutcome({ isMissing: true }, drawer)).toBe("notify");
147
+ expect(deepLinkOutcome({ isMissing: false }, panel)).toBe("notify");
148
+ expect(deepLinkOutcome({ isMissing: false }, drawer)).toBe("notify");
149
+ expect(deepLinkOutcome({ isMissing: false, isFailed: true }, panel)).toBe(
150
+ "notifyFailed",
151
+ );
152
+ expect(deepLinkOutcome({ isMissing: false, isFailed: true }, drawer)).toBe(
153
+ "notifyFailed",
154
+ );
155
+ });
133
156
  });
134
157
 
135
158
  describe("action targets on a selection and on a listed row", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms",
3
- "version": "0.7.13",
3
+ "version": "0.7.15",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/AntelopeJS/dms.git"
@@ -37,7 +37,7 @@
37
37
  "@antelopejs/interface-data-api": ">=0.2.0 <1.0.0",
38
38
  "@antelopejs/interface-database": ">=0.1.8 <1.0.0",
39
39
  "@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
40
- "@antelopejs/interface-dms": ">=0.5.8 <0.6.0",
40
+ "@antelopejs/interface-dms": ">=0.5.10 <0.6.0",
41
41
  "@antelopejs/interface-email": ">=0.0.5 <1.0.0",
42
42
  "@antelopejs/interface-file-storage": ">=0.1.3 <1.0.0",
43
43
  "archiver": "^7.0.1",
@@ -86,6 +86,9 @@ An absolute link (a public file, a snippet to copy, a mail) is built on `await G
86
86
  (`@antelopejs/interface-dms/api-base-url`, the backend) or `await GetClientBaseUrl()`
87
87
  (`/client-base-url`, the dashboard), never on `Host` / `X-Forwarded-*`: a reverse proxy may not
88
88
  forward them. Both answer the DMS config (`apiBaseUrl`, `clientBaseUrl`).
89
+ A client address (a rate limit, an anonymous visitor) comes from `await getClientIp(context)`
90
+ (`/client-ip`), never from `X-Forwarded-For` read by hand: it follows the DMS's
91
+ `auth.trustedProxies`, so the operator declares the proxies once.
89
92
 
90
93
  ## Navigation & access — module pages are owner-only
91
94