domma-cms 1.6.38 → 1.6.39
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.
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/package.json +2 -2
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/oauth-panel.js +303 -0
- package/plugins/_lib/admin/mail/reader-view.js +126 -13
- package/plugins/_lib/mail/accounts.js +548 -225
- package/plugins/_lib/mail/imapPool.js +63 -13
- package/plugins/_lib/mail/oauth.js +453 -0
- package/plugins/_lib/mail/oauthRoutes.js +294 -0
- package/plugins/_lib/mail/readRoutes.js +23 -2
- package/plugins/_lib/mail/scheduler.js +30 -6
- package/plugins/feedback/CLAUDE.md +7 -0
- package/plugins/feedback/admin/views/feedback.js +1 -1
- package/plugins/feedback/lib/sender.js +19 -0
- package/plugins/feedback/plugin.js +2 -0
- package/plugins/feedback/plugin.json +2 -2
- package/plugins/feedback/tests/shape.test.js +23 -1
- package/plugins/free-tier.lock.json +19 -19
- package/plugins/shopping-cart/CLAUDE.md +7 -0
- package/plugins/shopping-cart/admin/views/orders.js +68 -48
- package/plugins/shopping-cart/plugin.json +1 -1
- package/plugins/shopping-cart/tests/pure.test.js +16 -0
- package/plugins/support/CLAUDE.md +7 -0
- package/plugins/support/admin/views/support.js +1 -1
- package/plugins/support/lib/sender.js +15 -0
- package/plugins/support/plugin.json +2 -2
- package/plugins/support/tests/shape.test.js +25 -1
- package/server/routes/outreach-public.js +2 -2
- package/server/services/request-log.js +3 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"note": "Written by `make sync-free-tier` in dcms-marketplace. Do not edit these plugins here - edit them there and sync.",
|
|
3
|
-
"source": "dcms-marketplace@
|
|
3
|
+
"source": "dcms-marketplace@0525295",
|
|
4
4
|
"plugins": {
|
|
5
5
|
"blog": {
|
|
6
6
|
"version": "1.9.7",
|
|
@@ -118,14 +118,14 @@
|
|
|
118
118
|
}
|
|
119
119
|
},
|
|
120
120
|
"feedback": {
|
|
121
|
-
"version": "1.1.
|
|
121
|
+
"version": "1.1.8",
|
|
122
122
|
"files": {
|
|
123
|
-
"CLAUDE.md": "
|
|
123
|
+
"CLAUDE.md": "8666d4ef8d8620f0fc20f544a4c1915cddc21370cbc27a7f854ebfe41f4cfe1d",
|
|
124
124
|
"admin/css/index.css": "c8f827ad62386134e87bc05bd2c8e24f41184402b0a5a17e7250d749240c34c0",
|
|
125
125
|
"admin/lib/kit.js": "502d2d090e8aaeda03034864b4e3cc0d9bbf7e8fba29b6e4916a8a758c0cd54d",
|
|
126
126
|
"admin/lib/settings.js": "6e3fe9103fcbf7acb52211698ca408fac067e061100e580a9a3011cd76bdfba3",
|
|
127
127
|
"admin/templates/feedback.html": "a366dce78f6c3e84a7cb0ac63eba53e1980a4eb44154496a0536f5214c5bb36f",
|
|
128
|
-
"admin/views/feedback.js": "
|
|
128
|
+
"admin/views/feedback.js": "5aed85f09ef8af762d425912ca7fb4299ec129a21822863edf431af4f1a12e64",
|
|
129
129
|
"admin/views/inbox.js": "20cc1bd16df59c775f0e2d398f4179d0e30683a3a76a942e250f59f7cbdce98a",
|
|
130
130
|
"admin/views/send.js": "104290b8f58f7e8d75d86de0b3f5496bd38546b6bfb20ddff45e153e61f7d061",
|
|
131
131
|
"collections/feedback-reports.json": "a072291b52463f7785a77f5f77d36b8238ba8f89cfc2672c6642eccdb3144486",
|
|
@@ -134,13 +134,13 @@
|
|
|
134
134
|
"lib/attachments.js": "b23bda973c2e7723bc485c5f2840868363a3ac4e65c153cb5abd5a3c41dba3ad",
|
|
135
135
|
"lib/cache.js": "6db2859c21a3dddcc2ec34e8380dd672fc99a6cb0d76a2b05b68086503f4ccce",
|
|
136
136
|
"lib/receiver.js": "98aeea6712ba6aa576919520a86451f7f223e5154416a225a848e0c875923c8d",
|
|
137
|
-
"lib/sender.js": "
|
|
137
|
+
"lib/sender.js": "58194d1ab562e55e7ac6a29aed6b5ec8316ec6932b40b44034bb347e8f799f79",
|
|
138
138
|
"lib/shape.js": "47ccbbb95598023f8de03dd0f557205159375fd2bb61ee799a19c186c45bc4d9",
|
|
139
|
-
"plugin.js": "
|
|
140
|
-
"plugin.json": "
|
|
139
|
+
"plugin.js": "dd9cf5066e8491184a782112ed9463a211b37c2dbd93caf832edd65c2bb0e2a4",
|
|
140
|
+
"plugin.json": "9712d3eba3d81a0a8f5f85dfa168ea158d14b6ef9c0de6416314938c1b41f6ed",
|
|
141
141
|
"tests/api.test.js": "67336ed4a034e21ead5876becefa1de2fa2229b06689385471516df92e198d33",
|
|
142
142
|
"tests/settings.test.js": "3a50ac89a9d114c8466fc043a66f0f1bf373e7a7ad6dba45a995468c3332b374",
|
|
143
|
-
"tests/shape.test.js": "
|
|
143
|
+
"tests/shape.test.js": "2e2e5e17ce059fbaa4dad8170ae45382d6c7e74bacf3b301814bb81a60ba98ba"
|
|
144
144
|
}
|
|
145
145
|
},
|
|
146
146
|
"mail-reader": {
|
|
@@ -181,14 +181,14 @@
|
|
|
181
181
|
}
|
|
182
182
|
},
|
|
183
183
|
"shopping-cart": {
|
|
184
|
-
"version": "1.6.
|
|
184
|
+
"version": "1.6.3",
|
|
185
185
|
"files": {
|
|
186
|
-
"CLAUDE.md": "
|
|
186
|
+
"CLAUDE.md": "1c881307d422beaeaef405b9d90ec4c71679fdebe0a919353fbd9c597e26b22b",
|
|
187
187
|
"admin/css/index.css": "dcb2d30126c2e049b03a2e2b615082a067ab4dc911c87bf128029c900e6691b4",
|
|
188
188
|
"admin/lib/chart.js": "b01ce5cfac773c8df8efccab50b67d3dfa99fbead0037d817553db4a6688d4d6",
|
|
189
189
|
"admin/lib/kit.js": "d9aa90469b3f589beaff0c3fb6ad3d983ab3fd6d94c06965be639a665bfe052f",
|
|
190
190
|
"admin/templates/shop.html": "c5bb03f1f7cbabfbd8202f3533783eb1848103755b1ba25fdcf1fd8e9478cb8e",
|
|
191
|
-
"admin/views/orders.js": "
|
|
191
|
+
"admin/views/orders.js": "1c5b571027c78223f98dec2a39c1af8e33e0d810a9b34e0b176ebf85c5c9f486",
|
|
192
192
|
"admin/views/overview.js": "97045c069c6560b3f67e1b2230bdc9c4133ed92ec6f732886470f886f11c755a",
|
|
193
193
|
"admin/views/products.js": "e109660355aaec8db07eb65fa1169e7d2ec20bc2ec45293cc20e34f7c1bd62bf",
|
|
194
194
|
"admin/views/sales.js": "da71b9fbb7d39e39ecd8be35b1187e88bc4b5b28f18baaeddc6ea4cd202bb330",
|
|
@@ -211,7 +211,7 @@
|
|
|
211
211
|
"lib/shop.js": "baa5dfc4e410210e8ba13678997ee5752eb2ebe72fc4d3e449465d29b60b458d",
|
|
212
212
|
"lib/summary.js": "cdb13ce3d1799d429059fa03f179281711dc00427ccf2eb6b8f8aa52fef26295",
|
|
213
213
|
"plugin.js": "824dba8336846d443b2427ed9a5adeba11740570566fc6e89257f9847a488632",
|
|
214
|
-
"plugin.json": "
|
|
214
|
+
"plugin.json": "475c64d3e5f07d0dd4f1480b65630d0e9ef362418653477de9be617aac13dc10",
|
|
215
215
|
"plugin.public.js": "2f92e8340c9da653401c11dd99dab033dc6772d63fa2114489192c48e9e0be00",
|
|
216
216
|
"public/account.js": "5f644016c098c7879c59948d662f5997cb61680996204ecb7897baab8945d434",
|
|
217
217
|
"public/cart.js": "7338141392ec653ff616eb75688d2a2ad08cff8427d7020d381f540be0037b73",
|
|
@@ -237,22 +237,22 @@
|
|
|
237
237
|
"tests/accounts.test.js": "e64adc2a855d4ca017682bba7f75b052534f826556d8d35d825fb79cbc66596a",
|
|
238
238
|
"tests/api.test.js": "15c71751a3d31171707cbb3119d21b85c1c59fb7a67272b318dd4655809ac8d2",
|
|
239
239
|
"tests/commission.test.js": "fbc4cf83e0ed6446a09e57e4ff37a9608c81947c633c78ac6049eb0aedf6cb58",
|
|
240
|
-
"tests/pure.test.js": "
|
|
240
|
+
"tests/pure.test.js": "59a351c813784e8131b96a118c79de2e3858a2fba0fc5c0a6baed79e28cafda8",
|
|
241
241
|
"tests/refunds.test.js": "0ae06afca3cf9fd1fe89f1fbc63b3676216c79e174dbcb83788180c01d078366",
|
|
242
242
|
"tests/sales.test.js": "9025bc608b04bcc14f5bb2c98b4f7c35de99f3ddab4ffa59772d045795bf2cb7"
|
|
243
243
|
}
|
|
244
244
|
},
|
|
245
245
|
"support": {
|
|
246
|
-
"version": "1.1.
|
|
246
|
+
"version": "1.1.6",
|
|
247
247
|
"files": {
|
|
248
|
-
"CLAUDE.md": "
|
|
248
|
+
"CLAUDE.md": "65845bbf6a909cd523466896874e84a2a29d239fdc5da0840df5155913cdcf23",
|
|
249
249
|
"admin/css/index.css": "6695029b1da2a2e4ff25db4abecd360895ed39e4a9bc7f0996857672faf2c426",
|
|
250
250
|
"admin/lib/kit.js": "3d5a5fb5c53f7cc9ff98664803ff58701a4d8598068e9f1a16e9db0a72109c40",
|
|
251
251
|
"admin/lib/settings.js": "3f0b7c4c5ab927b2785af2abb3872ffd9269d12b402468f79795c4198eb49ea0",
|
|
252
252
|
"admin/templates/support.html": "2bb69d0443a367a402e6160fe68f4af3e8983b578cfff9575b4df9a534fcb480",
|
|
253
253
|
"admin/views/desk.js": "4f3cffe3603592d715c6a9b553e02dc7331a2675cabaeddff709c91436531734",
|
|
254
254
|
"admin/views/inbox.js": "8134d91e20a19eb2ca7b25edb604fa241e65b764e0d058ee5d8751f903a16c32",
|
|
255
|
-
"admin/views/support.js": "
|
|
255
|
+
"admin/views/support.js": "d1c72cc2e35609e54be296791cdcceb43be9be016b6c307000ddb8a226ba6d92",
|
|
256
256
|
"collections/support-tickets.json": "55a0856bacd66c2e8f1e3c85bdf3638e5ff826560943cda854a8f2d336a88905",
|
|
257
257
|
"config.js": "eb7ccc1169fa8b2be52a213536825492fedaec5fa73fd749cba9f498dd54b503",
|
|
258
258
|
"docs/guide.md": "bfe570f4f485f58ae3aab4f71039e5e426f85f6f348c96794dfcb2dc494fced1",
|
|
@@ -260,13 +260,13 @@
|
|
|
260
260
|
"lib/cache.js": "70cc826b24cc5359597a5c83a990f6da43cadebf2ac34094aee42058240fb72d",
|
|
261
261
|
"lib/mail.js": "d14aae3a59aad6af61451863cdaaf94fc4a87810b16e1dcb9c655dda6c803b3b",
|
|
262
262
|
"lib/receiver.js": "c9401cc799e5eafd419c136c95826ca28a8455e32e99cea32f4821acc1fb6c26",
|
|
263
|
-
"lib/sender.js": "
|
|
263
|
+
"lib/sender.js": "2b308422ac60a8a2801570e06319d5972797a601640c67beee95ccab9ac64092",
|
|
264
264
|
"lib/shape.js": "d60b48410aa5582ea80451c3d831686b0e29525a958733a8bc9dfaae8eca94d2",
|
|
265
265
|
"plugin.js": "91fff8d66bf5fa0fa4502bb177460ae234f0ed0494a4fbe6dd94d63e2f88b571",
|
|
266
|
-
"plugin.json": "
|
|
266
|
+
"plugin.json": "70bf4f117c4b2f9132365f73a1b1a38c3dfd5618d5273a8c899a2393323ebf43",
|
|
267
267
|
"tests/api.test.js": "1af65f0cba8a83c080a6e3e65544abc86fc43c8e25ca05c7f3e1ab4e3f7ac5a2",
|
|
268
268
|
"tests/settings.test.js": "434acbfd940eef800b1166a9aaf36363351af5652fcfb06527b8498866b7b964",
|
|
269
|
-
"tests/shape.test.js": "
|
|
269
|
+
"tests/shape.test.js": "7866c6c1d140716bf0817449937d6d711d7c3b97be1db1e1eb33bde341e36296"
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
}
|
|
@@ -243,3 +243,10 @@ Cart only READS it (`lib/commission.js currentCommission`, re-read when the file
|
|
|
243
243
|
`saveSettings` move it too. `GET /count` (every admin tab, every minute, all orders + products) keeps one answer for
|
|
244
244
|
60 s unless the generation moved; an answer worked out across a write is not kept. `services.writeGeneration` lets
|
|
245
245
|
extensions (Takeaway 0.4.2) cache their own order reads the same way - feature-detect it, older Carts lack it.
|
|
246
|
+
|
|
247
|
+
## 1.6.3 (2026-10-13): the Refund form was empty
|
|
248
|
+
`E.confirm` sanitises its message (preset basic), which strips `<input>` - so 1.6.2's Refund dialog showed only the words,
|
|
249
|
+
no amount, reason or restock box, and OK refunded the WHOLE order. `refund()` in `admin/views/orders.js` is now a small
|
|
250
|
+
slideover (`openPanel`, like Fulfil): amount (prefilled with what is left), reason, restock box (greyed out unless the
|
|
251
|
+
amount is all that is left), a Refund button. Never put form fields in `E.confirm` / `E.alert`.
|
|
252
|
+
`tests/pure.test.js` "admin: no form fields inside E.confirm" guards it across the admin views.
|
|
@@ -172,54 +172,74 @@ export async function mount(root, ctx) {
|
|
|
172
172
|
/** Money already given back on an order (legacy whole-order refunds included). */
|
|
173
173
|
const refundedOf = (d) => Array.isArray(d.refunds) ? d.refunds.reduce((n, r) => n + (Number(r.amount) || 0), 0) : d.status === 'refunded' ? d.total || 0 : 0;
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Refund - all that is left, or part of it - in a small form of its own. (Not E.confirm: its message is
|
|
177
|
+
* sanitised, which strips inputs, so the amount, reason and restock box never showed and OK refunded it all.)
|
|
178
|
+
*/
|
|
179
|
+
function refund(o) {
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
const d = o.data;
|
|
182
|
+
const via = {stripe: 'through Stripe', paypal: 'through PayPal'}[d.provider];
|
|
183
|
+
const before = refundedOf(d);
|
|
184
|
+
const left = Math.max(0, (d.total || 0) - before);
|
|
185
|
+
const f = {amount: M.observable(moneyIn(left)), note: M.observable(''), saving: M.observable(false)};
|
|
186
|
+
let restock = Boolean(d.stockTaken);
|
|
187
|
+
const full = () => moneyOut(f.amount.peek()) === left;
|
|
188
|
+
let panel = null;
|
|
189
|
+
let box = null;
|
|
190
|
+
// Stock only comes back when the whole order is refunded, so the box greys out on a part refund.
|
|
191
|
+
const syncBox = () => { if (box && d.stockTaken) { box.disabled = !full(); box.checked = full() && restock; } };
|
|
192
|
+
const stop = M.effect(() => { f.amount.value; syncBox(); });
|
|
193
|
+
openPanel({
|
|
194
|
+
title: `${before ? 'Refund more of' : 'Refund'} ${orderLabel(d)}`,
|
|
195
|
+
size: 'sm',
|
|
196
|
+
model: f,
|
|
197
|
+
html: `<div class="shp-form">
|
|
198
|
+
<p class="shp-note">${before ? `<strong>${esc(money(left))}</strong> of ${esc(money(d.total))} is left to refund (${esc(money(before))} already refunded).` : `The order came to <strong>${esc(money(d.total))}</strong>.`}</p>
|
|
199
|
+
<label class="form-label" for="shp-refund-amount">Amount <span data-help="Leave the whole amount for a full refund. A smaller amount is a part refund: the order stays as it is, with what was given back on it, and you can refund the rest later."></span></label>
|
|
200
|
+
<input class="form-input" id="shp-refund-amount" inputmode="decimal" data-model="amount.value" aria-describedby="shp-refund-hint">
|
|
201
|
+
<p class="shp-muted" id="shp-refund-hint">${via && d.paymentRef ? `The money goes back ${via} straight away.` : 'This only records the refund - give the money back the way it came.'}</p>
|
|
202
|
+
<label class="form-label" for="shp-refund-note">Reason <span class="shp-muted">(optional, kept on the order)</span></label>
|
|
203
|
+
<input class="form-input" id="shp-refund-note" maxlength="200" data-model="note.value" placeholder="Damaged in the post, one item missing…">
|
|
204
|
+
<label class="shp-check"><input type="checkbox" id="shp-restock"> Put the items back in stock <span class="shp-muted">(full refund only)</span></label>
|
|
205
|
+
<div class="shp-actions"><span class="shp-spacer"></span>
|
|
206
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
207
|
+
<button type="button" class="btn btn-sm btn-danger" data-on-click="save" data-bind-disabled="saving.value"><span data-icon="credit-card" data-icon-size="13"></span> Refund</button></div>
|
|
208
|
+
</div>`,
|
|
209
|
+
before: (el) => {
|
|
210
|
+
box = el.querySelector('#shp-restock');
|
|
211
|
+
if (!d.stockTaken) box.disabled = true;
|
|
212
|
+
box.addEventListener('change', () => { restock = box.checked; });
|
|
213
|
+
syncBox();
|
|
214
|
+
},
|
|
215
|
+
cleanup: () => { stop?.(); resolve(null); },
|
|
216
|
+
methods: {
|
|
217
|
+
cancel: () => panel?.close(true),
|
|
218
|
+
async save() {
|
|
219
|
+
if (f.saving.peek()) return;
|
|
220
|
+
const amount = moneyOut(f.amount.peek());
|
|
221
|
+
if (!Number.isInteger(amount) || amount <= 0 || amount > left) {
|
|
222
|
+
E.toast(`Enter an amount between ${money(1)} and ${money(left)}.`, {type: 'error'});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const all = amount === left;
|
|
226
|
+
f.saving.value = true;
|
|
227
|
+
try {
|
|
228
|
+
const r = await api.orders.refund(o.id, {amount: all ? undefined : amount, restock: all && restock, note: f.note.peek()});
|
|
229
|
+
replace(r.order);
|
|
230
|
+
refreshBadge();
|
|
231
|
+
E.toast(`${orderLabel(d)} ${r.how}.`, {type: 'success'});
|
|
232
|
+
resolve(r.order);
|
|
233
|
+
panel?.close(true);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
E.toast(err.message || String(err), {type: 'error'});
|
|
236
|
+
} finally {
|
|
237
|
+
f.saving.value = false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}).then(p => { panel = p; p.panel.querySelector('#shp-refund-amount')?.focus(); });
|
|
242
|
+
});
|
|
223
243
|
}
|
|
224
244
|
|
|
225
245
|
async function remove(o) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shopping-cart",
|
|
3
3
|
"displayName": "Shopping Cart",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.3",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Sell from your site: products with options, stock and pictures, a storefront and a cart that follows shoppers from page to page, and checkout with Stripe, PayPal or payment offline. Orders to fulfil in the sidebar, refunds from the order.",
|
|
7
7
|
"author": "Domma CMS",
|
|
@@ -313,3 +313,19 @@ test('payments: Stripe refunds and sessions carry an Idempotency-Key, so a retry
|
|
|
313
313
|
assert.equal(seen[2].init.headers['Idempotency-Key'], 'shop-session-o1');
|
|
314
314
|
} finally { globalThis.fetch = realFetch; }
|
|
315
315
|
});
|
|
316
|
+
|
|
317
|
+
test('admin: no form fields inside E.confirm (its message is sanitised; inputs vanish)', async () => {
|
|
318
|
+
const {readFileSync, readdirSync} = await import('node:fs');
|
|
319
|
+
const dir = new URL('../admin/views/', import.meta.url);
|
|
320
|
+
for (const f of readdirSync(dir).filter(n => n.endsWith('.js'))) {
|
|
321
|
+
const src = readFileSync(new URL(f, dir), 'utf8');
|
|
322
|
+
for (const m of src.matchAll(/E\.confirm\(\s*([A-Za-z_$][\w$]*)\s*[,)]/g)) {
|
|
323
|
+
// A variable passed to E.confirm: find its last assignment before the call and refuse form fields in it.
|
|
324
|
+
const before = src.slice(0, m.index);
|
|
325
|
+
const at = before.lastIndexOf(`${m[1]} =`);
|
|
326
|
+
const assigned = at < 0 ? '' : before.slice(at, at + 4000);
|
|
327
|
+
assert.ok(!/<(input|select|textarea)\b/.test(assigned), `${f}: E.confirm(${m[1]}) carries form fields`);
|
|
328
|
+
}
|
|
329
|
+
for (const m of src.matchAll(/E\.confirm\(`([^`]*)`/g)) assert.ok(!/<(input|select|textarea)\b/.test(m[1]), `${f}: E.confirm with form fields`);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
@@ -133,3 +133,10 @@ copy of the pure rules (lib/shape.js) and the store (lib/attachments.js) since e
|
|
|
133
133
|
- `admin/lib/settings.js`: the cog, added by `support.js` once the mode is known, only when the silent
|
|
134
134
|
`GET /settings` answers; slideover through kit's `openPanel`, M bindings, data-help per field, discard guard,
|
|
135
135
|
Ctrl+Enter. Test: tests/settings.test.js.
|
|
136
|
+
|
|
137
|
+
## 1.1.6 (2026-10-13) - a request deleted on the manager takes the site's files with it
|
|
138
|
+
The site keeps a copy of each file it sent (`outbox.files`, newest 2000) to draw its own thread, and the prune spared
|
|
139
|
+
every id on that list - so a request Domma deleted left its screenshots on the site for good. `remember(remote)` now
|
|
140
|
+
narrows `files` with `keepFiles(files, remote, items)` (PURE, tested): only ids in a message of a request the manager
|
|
141
|
+
still holds (its `/fleet/mine` is complete for the site) or named by an outbox item. The next prune (older than a day)
|
|
142
|
+
removes the rest. Found on the dcm click-through.
|
|
@@ -139,6 +139,9 @@ export function createOutbox(dataDir) {
|
|
|
139
139
|
const known = latest.has(ref);
|
|
140
140
|
if ((!known && !local.has(ref)) || (known && (latest.get(ref) || '') !== (entry.dommaAt || ''))) delete s.shelf[ref];
|
|
141
141
|
}
|
|
142
|
+
// Files this site keeps to show its own thread: only those the manager still has (a request
|
|
143
|
+
// deleted there takes its files with it) or an item here still names. The next prune removes the rest.
|
|
144
|
+
s.files = keepFiles(s.files, remote, s.items);
|
|
142
145
|
const byRef = new Map(remote.map(t => [t.ref, t]));
|
|
143
146
|
const have = new Set(remote.flatMap(t => [t.ref, ...(t.messages || []).map(m => m.id)]));
|
|
144
147
|
s.items = s.items.filter((i) => {
|
|
@@ -157,6 +160,18 @@ export function createOutbox(dataDir) {
|
|
|
157
160
|
};
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Of the file ids this site kept after sending them, those still wanted: in a message of a request the
|
|
165
|
+
* manager still holds (its list for this site is complete), or named by an outbox item. PURE.
|
|
166
|
+
*/
|
|
167
|
+
export function keepFiles(files, remote, items = []) {
|
|
168
|
+
const live = new Set([
|
|
169
|
+
...(remote || []).flatMap(t => (t.messages || []).flatMap(m => (m.attachments || []).map(a => a.id))),
|
|
170
|
+
...(items || []).flatMap(i => (i.payload?.attachments || []).map(a => a.id))
|
|
171
|
+
]);
|
|
172
|
+
return (files || []).filter(id => live.has(id));
|
|
173
|
+
}
|
|
174
|
+
|
|
160
175
|
/** Has every file an item announced been sent, or given up on? */
|
|
161
176
|
const filesDone = (i) => {
|
|
162
177
|
const done = new Set([...(i.filesSent || []), ...(i.filesRefused || [])]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "support",
|
|
3
3
|
"displayName": "Support",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.6",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Get help from Domma straight from your admin: our email, phone and hours always to hand, and a conversation with the Domma team for anything you need - urgent requests first. Right-click a request to archive, resolve or remove it.",
|
|
7
7
|
"author": "Domma CMS",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
],
|
|
62
62
|
"views": {
|
|
63
63
|
"plugin-support": {
|
|
64
|
-
"entry": "support/admin/views/support.js?v=1.1.
|
|
64
|
+
"entry": "support/admin/views/support.js?v=1.1.6",
|
|
65
65
|
"exportName": "supportView"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
addTurn, buildMessagePayload, buildTicketPayload, cleanRequest, cleanUpdate, contactCard, createThrottle, lastDommaAt,
|
|
6
6
|
receivedTicket, senderView, telHref, toCustomerEmail, toDommaEmail
|
|
7
7
|
} from '../lib/shape.js';
|
|
8
|
-
import {applyShelf, createOutbox, flush, mergeTickets} from '../lib/sender.js';
|
|
8
|
+
import {applyShelf, createOutbox, flush, keepFiles, mergeTickets} from '../lib/sender.js';
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
@@ -237,3 +237,27 @@ test('a sent message stays in the outbox until its files have gone', () => {
|
|
|
237
237
|
assert.ok(ob.fileIds().has(file(1).id), 'the file itself is still wanted: the thread shows it');
|
|
238
238
|
} finally { fs.rmSync(dir, {recursive: true, force: true}); }
|
|
239
239
|
});
|
|
240
|
+
|
|
241
|
+
test('files kept for the thread go when the manager no longer has their request', () => {
|
|
242
|
+
const remote = [{ref: 't1', messages: [{id: 'm1', attachments: [{id: 'f1'}]}, {id: 'm2'}]}];
|
|
243
|
+
const items = [{ref: 'm9', payload: {attachments: [{id: 'f3'}]}}];
|
|
244
|
+
assert.deepEqual(keepFiles(['f1', 'f2', 'f3'], remote, items), ['f1', 'f3'], 'f2 was on a request deleted at the manager');
|
|
245
|
+
assert.deepEqual(keepFiles(['f1'], [], []), []);
|
|
246
|
+
assert.deepEqual(keepFiles(undefined, remote), []);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('remember(): a deleted request takes its files out of the keep list', () => {
|
|
250
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sup-outbox-'));
|
|
251
|
+
try {
|
|
252
|
+
const box = createOutbox(dir);
|
|
253
|
+
box.add('ticket', {ref: 't1', attachments: [{id: 'f1'}]});
|
|
254
|
+
box.mark('t1', {state: 'sent'});
|
|
255
|
+
box.fileSent('t1', 'f1');
|
|
256
|
+
box.remember([{ref: 't1', messages: [{id: 't1', attachments: [{id: 'f1'}]}]}], null);
|
|
257
|
+
assert.ok(box.fileIds().has('f1'), 'still on the manager: kept');
|
|
258
|
+
box.remember([], null);
|
|
259
|
+
assert.equal(box.fileIds().has('f1'), false, 'deleted on the manager: no longer kept');
|
|
260
|
+
} finally {
|
|
261
|
+
fs.rmSync(dir, {recursive: true, force: true});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
@@ -40,7 +40,7 @@ const badLink = () => page('Unsubscribe', '<h1>That link is not right.</h1><p>Us
|
|
|
40
40
|
|
|
41
41
|
/** The confirm page: the same e and k, posted back by one button. */
|
|
42
42
|
function confirmPage(email, e, k) {
|
|
43
|
-
return page('Unsubscribe', `<h1>Unsubscribe?</h1><p>${esc(email)} will get no more campaign or
|
|
43
|
+
return page('Unsubscribe', `<h1>Unsubscribe?</h1><p>${esc(email)} will get no more campaign, survey or onboarding emails from this site.</p>`
|
|
44
44
|
+ `<form method="post" action="${UNSUBSCRIBE_PATH}"><input type="hidden" name="e" value="${esc(e)}"><input type="hidden" name="k" value="${esc(k)}">`
|
|
45
45
|
+ `<button type="submit" class="btn btn-primary">Unsubscribe</button></form>`);
|
|
46
46
|
}
|
|
@@ -107,6 +107,6 @@ export async function outreachPublicRoutes(fastify, opts = {}) {
|
|
|
107
107
|
if (!opts.col) await ensureList();
|
|
108
108
|
await suppression.add(email, 'unsubscribe-link');
|
|
109
109
|
return html(reply).send(page('Unsubscribed',
|
|
110
|
-
`<h1>You are unsubscribed.</h1><p>${esc(email)} will get no more campaign or
|
|
110
|
+
`<h1>You are unsubscribed.</h1><p>${esc(email)} will get no more campaign, survey or onboarding emails from this site.</p>`));
|
|
111
111
|
});
|
|
112
112
|
}
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/** Parameter names redacted whatever their case. */
|
|
15
|
-
|
|
15
|
+
// `code` and `state`: an OAuth2 callback (the mail sign-in) carries a live
|
|
16
|
+
// authorisation code and its one-time ticket in the query string.
|
|
17
|
+
const SENSITIVE = new Set(['token', 't', 'key', 'hostkey', 'invitekey', 'code', 'state']);
|
|
16
18
|
|
|
17
19
|
/** Anything with secret or password in the name is redacted too. */
|
|
18
20
|
const SENSITIVE_PATTERN = /secret|password|passwd/i;
|