domma-cms 1.6.35 → 1.6.36
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/package.json +1 -1
- package/plugins/_lib/call/ice.js +5 -2
- package/plugins/_lib/call/rooms.js +9 -3
- package/plugins/_lib/call/routes.js +7 -4
- package/plugins/_lib/mail/defaults.js +12 -0
- package/plugins/_lib/mail/hostGuard.js +86 -0
- package/plugins/_lib/mail/readRoutes.js +20 -1
- package/plugins/_lib/public/call/call.js +3 -1
- package/plugins/blog/CLAUDE.md +11 -1
- package/plugins/blog/docs/guide.md +1 -1
- package/plugins/blog/lib/page.js +32 -0
- package/plugins/blog/plugin.js +58 -16
- package/plugins/blog/plugin.json +2 -2
- package/plugins/blog/plugin.public.js +5 -4
- package/plugins/blog/tests/public.test.js +30 -1
- package/plugins/booking/CLAUDE.md +4 -1
- package/plugins/booking/docs/guide.md +1 -1
- package/plugins/booking/plugin.js +16 -21
- package/plugins/booking/plugin.json +2 -2
- package/plugins/booking/tests/api.test.js +26 -1
- package/plugins/feedback/CLAUDE.md +7 -2
- package/plugins/feedback/admin/views/feedback.js +1 -1
- package/plugins/feedback/lib/receiver.js +1 -1
- package/plugins/feedback/plugin.json +3 -3
- package/plugins/free-tier.lock.json +42 -41
- package/plugins/mail-reader/CLAUDE.md +6 -0
- package/plugins/mail-reader/docs/guide.md +1 -0
- package/plugins/mail-reader/plugin.json +2 -2
- package/plugins/shopping-cart/CLAUDE.md +17 -1
- package/plugins/shopping-cart/docs/guide.md +1 -1
- package/plugins/shopping-cart/lib/accounts.js +8 -3
- package/plugins/shopping-cart/lib/payments.js +7 -4
- package/plugins/shopping-cart/lib/shop.js +8 -3
- package/plugins/shopping-cart/plugin.js +77 -41
- package/plugins/shopping-cart/plugin.json +2 -2
- package/plugins/shopping-cart/public/account.js +2 -2
- package/plugins/shopping-cart/tests/accounts.test.js +10 -1
- package/plugins/shopping-cart/tests/api.test.js +23 -0
- package/plugins/shopping-cart/tests/pure.test.js +16 -1
- package/plugins/support/CLAUDE.md +6 -0
- package/plugins/support/admin/views/support.js +1 -1
- package/plugins/support/docs/guide.md +90 -0
- package/plugins/support/lib/receiver.js +1 -1
- package/plugins/support/plugin.json +3 -3
- package/server/routes/api/analytics.js +32 -0
- package/server/routes/api/collections.js +148 -15
- package/server/server.js +5 -1
- package/server/services/authChecks.js +8 -0
- package/server/services/collectionAccess.js +191 -0
- package/server/services/collections.js +8 -2
- package/server/services/request-log.js +70 -0
|
@@ -48,21 +48,23 @@ import {
|
|
|
48
48
|
} from './lib/store.js';
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* The
|
|
52
|
-
*
|
|
53
|
-
* a
|
|
54
|
-
*
|
|
51
|
+
* The site's address for links in emails and the admin: `site.baseUrl` (Settings > Site), else '' - a link is then
|
|
52
|
+
* left out and the log says so once. Until 1.6.3 a request's Host header stood in when the setting was empty, so a
|
|
53
|
+
* booker who could reach the server with a Host of their choosing (default vhost, direct IP, misconfigured proxy)
|
|
54
|
+
* got the owner an email whose "Start the call" button went to their domain carrying the host key. A Host header
|
|
55
|
+
* is the caller's to choose; it never decides where anyone's link points. `request` is accepted for the registry's
|
|
56
|
+
* callers (Pro) and ignored.
|
|
55
57
|
*/
|
|
56
|
-
let
|
|
57
|
-
|
|
58
|
-
function siteBase(request) {
|
|
58
|
+
let warnedNoBase = false;
|
|
59
|
+
function siteBase(request = null, log = null) {
|
|
59
60
|
let base = '';
|
|
60
61
|
try { base = String(getConfig('site')?.baseUrl || ''); } catch { /* no site config */ }
|
|
61
62
|
if (/^https?:\/\/[^/\s]+/i.test(base)) return base.replace(/\/+$/, '');
|
|
62
|
-
if (!
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
if (log && !warnedNoBase) {
|
|
64
|
+
warnedNoBase = true;
|
|
65
|
+
log.warn?.('[booking] Site URL is not set (Settings > Site): booking emails go out without links until it is.');
|
|
66
|
+
}
|
|
67
|
+
return '';
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
const manageUrl = (s, b, base) => `${base}${s.bookingPath}/manage/${b.id}?t=${b.token}`;
|
|
@@ -209,7 +211,7 @@ export default async function booking(fastify, options) {
|
|
|
209
211
|
async function tell(b, what, {request = null, toCustomer = true, toOwner = true} = {}) {
|
|
210
212
|
try {
|
|
211
213
|
const s = await liveSettings();
|
|
212
|
-
const base = siteBase(request);
|
|
214
|
+
const base = siteBase(request, log);
|
|
213
215
|
const title = siteTitle();
|
|
214
216
|
const link = base && b.token ? manageUrl(s, b, base) : '';
|
|
215
217
|
const may = customerMayChange(b, s) && (s.allowCancel || s.allowMove);
|
|
@@ -282,7 +284,7 @@ export default async function booking(fastify, options) {
|
|
|
282
284
|
registry.services = {
|
|
283
285
|
name: NAME, liveSettings, allBookings, allServices, allBookables, getBooking, getService, getBookable,
|
|
284
286
|
changeBooking, sendReminders,
|
|
285
|
-
/** The site's address: `site.baseUrl`, else
|
|
287
|
+
/** The site's address: `site.baseUrl`, else '' (never a request's Host). */
|
|
286
288
|
siteBase: (request = null) => siteBase(request),
|
|
287
289
|
/** A booking as the admin may see it (no customer token; an online one's host key as `hostUrl`). */
|
|
288
290
|
adminBooking,
|
|
@@ -352,7 +354,6 @@ export default async function booking(fastify, options) {
|
|
|
352
354
|
const id = await linkContact(customer, bk?.userId || s.contactsOwnerId, contactsOn()).catch(() => '');
|
|
353
355
|
if (id) Object.assign(b, (await changeBooking(b.id, {contactId: id})).booking);
|
|
354
356
|
}
|
|
355
|
-
if (request) siteBase(request);
|
|
356
357
|
if (notify) tell(b, 'made', {request, toOwner: source !== 'admin'});
|
|
357
358
|
registry.emit('made', b);
|
|
358
359
|
return b;
|
|
@@ -411,7 +412,7 @@ export default async function booking(fastify, options) {
|
|
|
411
412
|
};
|
|
412
413
|
}
|
|
413
414
|
|
|
414
|
-
fastify.get('/bootstrap', can('read'), async (
|
|
415
|
+
fastify.get('/bootstrap', can('read'), async () => bootstrap());
|
|
415
416
|
|
|
416
417
|
fastify.get('/count', can('read'), async () => {
|
|
417
418
|
const s = await liveSettings();
|
|
@@ -513,7 +514,6 @@ export default async function booking(fastify, options) {
|
|
|
513
514
|
const id = await linkContact(customer, owner, contactsOn()).catch(() => '');
|
|
514
515
|
if (id) Object.assign(b, (await changeBooking(b.id, {contactId: id})).booking);
|
|
515
516
|
}
|
|
516
|
-
siteBase(request);
|
|
517
517
|
if (body.notify !== false) tell(b, 'made', {request, toOwner: false});
|
|
518
518
|
registry.emit('made', b);
|
|
519
519
|
return reply.code(201).send(adminBooking(b));
|
|
@@ -536,7 +536,6 @@ export default async function booking(fastify, options) {
|
|
|
536
536
|
try {
|
|
537
537
|
const b = await placeBooking({service, candidates, rules, startAt: startAt.toISOString(), tz: s.timeZone, moveId: current.id,
|
|
538
538
|
force: body.force === true, fields: {by: request.user?.name || ''}});
|
|
539
|
-
siteBase(request);
|
|
540
539
|
if (body.notify !== false) tell(b, 'moved', {request, toOwner: false});
|
|
541
540
|
registry.emit('moved', b, current);
|
|
542
541
|
endCall(current).catch(() => {});
|
|
@@ -564,7 +563,6 @@ export default async function booking(fastify, options) {
|
|
|
564
563
|
if (!res) return fail(reply, 404, 'No such booking.');
|
|
565
564
|
const {booking: b, from} = res;
|
|
566
565
|
if (b.status !== from) {
|
|
567
|
-
siteBase(request);
|
|
568
566
|
if (b.status === 'cancelled' && body.notify !== false) tell(b, 'cancelled', {request, toOwner: false});
|
|
569
567
|
registry.emit(b.status === 'cancelled' ? 'cancelled' : 'status', b, from);
|
|
570
568
|
if (b.status === 'cancelled') endCall(b).catch(() => {});
|
|
@@ -754,7 +752,6 @@ export default async function booking(fastify, options) {
|
|
|
754
752
|
const id = await linkContact(customer, bk?.userId || s.contactsOwnerId, contactsOn()).catch(() => '');
|
|
755
753
|
if (id) Object.assign(b, (await changeBooking(b.id, {contactId: id})).booking);
|
|
756
754
|
}
|
|
757
|
-
siteBase(request);
|
|
758
755
|
tell(b, 'made', {request});
|
|
759
756
|
notice(b, 'made');
|
|
760
757
|
registry.emit('made', b);
|
|
@@ -793,7 +790,6 @@ export default async function booking(fastify, options) {
|
|
|
793
790
|
? `Bookings can be cancelled here up to ${s.changeNoticeHours} hours before. Please contact us instead.`
|
|
794
791
|
: 'This booking cannot be cancelled here. Please contact us instead.');
|
|
795
792
|
const {booking: done} = await changeBooking(b.id, {status: 'cancelled', reason: String(request.body?.reason || '').slice(0, 300)}, b.name);
|
|
796
|
-
siteBase(request);
|
|
797
793
|
tell(done, 'cancelled', {request});
|
|
798
794
|
notice(done, 'cancelled');
|
|
799
795
|
registry.emit('cancelled', done, 'confirmed');
|
|
@@ -817,7 +813,6 @@ export default async function booking(fastify, options) {
|
|
|
817
813
|
const {service, candidates, rules} = await candidatesFor(b.serviceId, b.bookableId);
|
|
818
814
|
try {
|
|
819
815
|
const moved = await placeBooking({service, candidates, rules, startAt: offered.start, tz: s.timeZone, moveId: b.id, fields: {by: b.name}});
|
|
820
|
-
siteBase(request);
|
|
821
816
|
tell(moved, 'moved', {request});
|
|
822
817
|
notice(moved, 'moved');
|
|
823
818
|
registry.emit('moved', moved, b);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "booking",
|
|
3
3
|
"displayName": "Booking",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.3",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Let people book appointments with you or your team, and rooms or equipment, from a booking page on your site. Opening hours, days off and buffers; confirmation and reminder emails with a link to change or cancel; a diary in the admin. Book straight from a contact or an email.",
|
|
7
7
|
"author": "Domma CMS",
|
|
8
|
-
"date": "2026-10-
|
|
8
|
+
"date": "2026-10-13",
|
|
9
9
|
"icon": "clipboard-check",
|
|
10
10
|
"uses": {
|
|
11
11
|
"contacts": "Customers become contacts; Book… on a contact and a Bookings panel on the contact card",
|
|
@@ -15,8 +15,10 @@ import {bookingRegistry} from '../lib/extensions.js';
|
|
|
15
15
|
import {addDays, atWallClock, wallClock} from '../public/lib/time.js';
|
|
16
16
|
|
|
17
17
|
const PLUGINS = path.resolve('config/plugins.json');
|
|
18
|
+
const SITE = path.resolve('config/site.json');
|
|
18
19
|
const CONTACTS = path.resolve('content/collections/contacts-contacts/data.json');
|
|
19
20
|
let savedContacts;
|
|
21
|
+
let savedSite;
|
|
20
22
|
const DIRS = ['booking-bookables', 'booking-services', 'booking-bookings'].map(c => path.resolve('content/collections', c));
|
|
21
23
|
const TZ = 'Europe/London';
|
|
22
24
|
let savedPlugins;
|
|
@@ -29,6 +31,9 @@ const related = {};
|
|
|
29
31
|
before(async () => {
|
|
30
32
|
savedPlugins = await fs.readFile(PLUGINS, 'utf8').catch(() => null);
|
|
31
33
|
savedContacts = await fs.readFile(CONTACTS, 'utf8').catch(() => null);
|
|
34
|
+
// Links in emails come from the Site URL alone (1.6.3) - the checkout's site.json has none.
|
|
35
|
+
savedSite = await fs.readFile(SITE, 'utf8');
|
|
36
|
+
await fs.writeFile(SITE, JSON.stringify({...JSON.parse(savedSite), baseUrl: 'https://site.test'}, null, 2) + '\n');
|
|
32
37
|
for (const d of DIRS) await fs.rm(d, {recursive: true, force: true});
|
|
33
38
|
setMailer(async (msg) => { sent.push(msg); });
|
|
34
39
|
app = Fastify();
|
|
@@ -54,6 +59,7 @@ after(async () => {
|
|
|
54
59
|
if (savedPlugins === null) await fs.rm(PLUGINS, {force: true});
|
|
55
60
|
else await fs.writeFile(PLUGINS, savedPlugins);
|
|
56
61
|
if (savedContacts !== null) await fs.writeFile(CONTACTS, savedContacts);
|
|
62
|
+
if (savedSite) await fs.writeFile(SITE, savedSite);
|
|
57
63
|
if (dataDir) await fs.rm(dataDir, {recursive: true, force: true});
|
|
58
64
|
});
|
|
59
65
|
|
|
@@ -353,8 +359,27 @@ test('online: a service an extension makes online gives a call - the customer an
|
|
|
353
359
|
assert.match(r.body.booking.callUrl, /\/booking\/call\//);
|
|
354
360
|
await new Promise(res => setTimeout(res, 50));
|
|
355
361
|
const toHost = sent.find(m => m.to === 'vic@example.com');
|
|
356
|
-
assert.match(toHost?.text || '', /Start the call:
|
|
362
|
+
assert.match(toHost?.text || '', /Start the call: https:\/\/site\.test\/\S+&h=/, 'the person booked gets the host link, on the Site URL');
|
|
357
363
|
assert.doesNotMatch(sent.find(m => m.to === 'web@example.com').text, /&h=/);
|
|
364
|
+
|
|
365
|
+
// 1.6.3: a booker's Host header never decides where the owner's link points - with no Site URL there is no link at all.
|
|
366
|
+
sent.length = 0;
|
|
367
|
+
await fs.writeFile(SITE, savedSite);
|
|
368
|
+
try {
|
|
369
|
+
const slot2 = (await call('GET', `/public/slots?service=${svc.id}&from=${day}&to=${day}`)).body.days[0].slots[5];
|
|
370
|
+
const forged = await app.inject({method: 'POST', url: '/public/book', headers: {host: 'evil.example'},
|
|
371
|
+
payload: {serviceId: svc.id, startAt: slot2.start, customer: {name: 'Forger', email: 'forger@example.com'}}});
|
|
372
|
+
assert.equal(forged.statusCode, 201, forged.body);
|
|
373
|
+
await new Promise(res => setTimeout(res, 50));
|
|
374
|
+
const owner = sent.find(m => m.to === 'vic@example.com');
|
|
375
|
+
assert.ok(owner, 'the owner is still told');
|
|
376
|
+
assert.doesNotMatch(owner.text, /evil\.example/);
|
|
377
|
+
assert.doesNotMatch(owner.text, /&h=/, 'the host key goes in no link whose base is not the configured one');
|
|
378
|
+
assert.doesNotMatch(sent.find(m => m.to === 'forger@example.com').text, /evil\.example/);
|
|
379
|
+
await call('DELETE', `/bookings/${forged.json().booking.id}`);
|
|
380
|
+
} finally {
|
|
381
|
+
await fs.writeFile(SITE, JSON.stringify({...JSON.parse(savedSite), baseUrl: 'https://site.test'}, null, 2) + '\n');
|
|
382
|
+
}
|
|
358
383
|
const early = await call('POST', `/call/${r.body.booking.callUrl.split('/call/')[1].split('?')[0]}/join`,
|
|
359
384
|
{b: r.body.booking.id, t: new URL(`http://x${r.body.booking.callUrl}`).searchParams.get('t')});
|
|
360
385
|
assert.equal(early.status, 425, 'not open days ahead');
|
|
@@ -98,8 +98,8 @@ Replying in the inbox to a `_manager` report you filed yourself marks it seen fo
|
|
|
98
98
|
- **Who sent it is never the browser's word.** The sender builds the payload from `request.user`; the
|
|
99
99
|
manager attributes a report to the slug the fleet TOKEN resolves to and ignores any `site` in the body.
|
|
100
100
|
A site can still vouch for any of its own admins - it cannot speak for another site.
|
|
101
|
-
- **Tokens
|
|
102
|
-
401 or ECONNREFUSED for a few seconds. That is why the outbox exists: a report is "sent or waiting",
|
|
101
|
+
- **Tokens survive a manager restart** (since 2026-10-04 fleetTokens.js keeps them in `site-manager/data/fleet-tokens.json`,
|
|
102
|
+
mode 0600) but a site being restarted still meets a 401 or ECONNREFUSED for a few seconds. That is why the outbox exists: a report is "sent or waiting",
|
|
103
103
|
never lost. Waiting ones go on the next `/sent` and on the timer (`syncMinutes`, default 60, which also
|
|
104
104
|
notices new replies and raises a `feedback:reply` notification). A retry carries the same `ref`, and the
|
|
105
105
|
manager answers the existing report (200) instead of making a second.
|
|
@@ -113,3 +113,8 @@ Replying in the inbox to a `_manager` report you filed yourself marks it seen fo
|
|
|
113
113
|
(`.fb-note` is flex) beats the browser's. `index.css` carries `.fb [hidden] {display: none !important}`.
|
|
114
114
|
- Only this machine's sites can reach dcm (the fleet token lives in this manager's memory). Sites on other
|
|
115
115
|
machines would need their own manager to relay with an installation key; not built.
|
|
116
|
+
|
|
117
|
+
## 1.1.4 (2026-10-13 Tool sweep hygiene)
|
|
118
|
+
|
|
119
|
+
- Cache-busters follow the manifest: `feedback.js?v=1.1.4` in plugin.json and `V = '1.1.4'` for the sub-views - bump
|
|
120
|
+
both with the version. Fleet-token note above corrected (tokens are on disk since 2026-10-04).
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import {api, loadKit, store} from '../lib/kit.js';
|
|
17
17
|
|
|
18
18
|
const ROUTE = /^#\/plugins\/feedback(\/|$)/;
|
|
19
|
-
const V = '1.1.
|
|
19
|
+
const V = '1.1.4';
|
|
20
20
|
const TABS = [
|
|
21
21
|
{key: 'inbox', label: 'Inbox', icon: 'inbox'},
|
|
22
22
|
{key: 'mine', label: 'My feedback', icon: 'send'}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* admins' names and emails.
|
|
8
8
|
*
|
|
9
9
|
* A site is who its fleet token says it is (site-manager/services/fleetTokens.js,
|
|
10
|
-
*
|
|
10
|
+
* minted when the manager forked it and kept in site-manager/data/fleet-tokens.json). The module is imported at
|
|
11
11
|
* call time and only exists where Site Manager does, which is also how this
|
|
12
12
|
* plugin knows it is on the manager.
|
|
13
13
|
*
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feedback",
|
|
3
3
|
"displayName": "Feedback",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.4",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Tell Domma what is broken, missing or good - straight from your admin - and see what became of it: seen, planned, in progress, done, with Domma's reply.",
|
|
7
7
|
"author": "Domma CMS",
|
|
8
|
-
"date": "2026-10-
|
|
8
|
+
"date": "2026-10-13",
|
|
9
9
|
"icon": "message-square",
|
|
10
10
|
"permissions": [
|
|
11
11
|
{
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
],
|
|
41
41
|
"views": {
|
|
42
42
|
"plugin-feedback": {
|
|
43
|
-
"entry": "feedback/admin/views/feedback.js?v=1.1.
|
|
43
|
+
"entry": "feedback/admin/views/feedback.js?v=1.1.4",
|
|
44
44
|
"exportName": "feedbackView"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
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@b1edb76",
|
|
4
4
|
"plugins": {
|
|
5
5
|
"blog": {
|
|
6
|
-
"version": "1.9.
|
|
6
|
+
"version": "1.9.6",
|
|
7
7
|
"files": {
|
|
8
|
-
"CLAUDE.md": "
|
|
8
|
+
"CLAUDE.md": "5164b58785b979b828a707ac94088d80eb1a1655fc48a23404aeca8cef316e70",
|
|
9
9
|
"admin/css/index.css": "6142a6b756e1218157842528830b1e84f592f0b84b7d5ce6449db421b6f4b039",
|
|
10
10
|
"admin/templates/blog.html": "a6054c1febe764bb1edf5c9e466d44a9080d9c83ba5486437b7a8244acf5ba54",
|
|
11
11
|
"admin/templates/categories.html": "ab7d6fc2fedd48876c585bc216a1532b3bdd78c946dee976c1171bfe4664feb6",
|
|
@@ -41,17 +41,17 @@
|
|
|
41
41
|
"collections/comments/schema.json": "29cbd289cbfcfc4158c9357a218db4ce06eddc3370bfbf9afb7d7f52065a849e",
|
|
42
42
|
"collections/posts/schema.json": "19a5a770d3eefea73f4117454bd1312a7a7e3d727cfcdfde141139fbc81cabd7",
|
|
43
43
|
"config.js": "ee67396262762f5c31269d258affd75f7392a3e364f72f7792c2f1cdafd94bba",
|
|
44
|
-
"docs/guide.md": "
|
|
44
|
+
"docs/guide.md": "d9f70afe4612a2464fc537a0348e987873d09cc30ce44692a22cf79c40a4c0b4",
|
|
45
45
|
"lib/access.js": "af47440c7233a56000641a3231d7e9ff81b12f40dfb9528aa6dd4e2dd9bca8df",
|
|
46
46
|
"lib/layouts.js": "dbb6ae9e514ccaf600a732702925b905131dea166d90fe759a62b6652e78e1de",
|
|
47
47
|
"lib/nav-link.js": "6a8f9368bb8aee1c21e90fb335a691859cb9a9bc250c2b9167a33ca9189fa2ff",
|
|
48
48
|
"lib/notices.js": "a9a2918ee6f5f7e0bbaeced0d3539584279bdbe68d15472a8615bdddb4ef905b",
|
|
49
|
-
"lib/page.js": "
|
|
49
|
+
"lib/page.js": "ca84803672c3815c9606402bd24e9c6bdef167f7a342eb6d3d51a8b9a9c692ba",
|
|
50
50
|
"lib/render.js": "0e97c6a950bc962c8d8ea4b2f21d06cb73fbcdc738d8942d6935d01309a29b1d",
|
|
51
51
|
"lib/samples.js": "e8cdb77ce22e43937707f6854d3a5cb722437dc584e22ac7b24480bef686cf85",
|
|
52
|
-
"plugin.js": "
|
|
53
|
-
"plugin.json": "
|
|
54
|
-
"plugin.public.js": "
|
|
52
|
+
"plugin.js": "278d269eb8fb6df2f64f23ae26b5e5b5fe96e7f9d07241539d261d66d4ccd7f7",
|
|
53
|
+
"plugin.json": "ff7466d42c41dd64117861d4d88914be1d367854ade67cb4ca42d8b42068bcf4",
|
|
54
|
+
"plugin.public.js": "33387fa7636b877dec3881b0698918bc1a7379d5690d010c826aedc4091284d8",
|
|
55
55
|
"public/blog.css": "fd733e6b624190da1e70dc1b6613e2376efd7f7570a28fab311d81006d03d960",
|
|
56
56
|
"public/blog.js": "d01c72a9834dcc7c5241923371f2b164ac30d20ac9f1a5c5dc5b71fa3cb09f08",
|
|
57
57
|
"public/samples/aurora.svg": "feb29e2b46465913fc29de9eda221b00b1541f1fa3ea7120f0c1d1cd11a9a4f6",
|
|
@@ -69,13 +69,13 @@
|
|
|
69
69
|
"tests/layouts.test.js": "554cff8194bd650cfb5f2a83c1f51c25f427c07f7dbfdc2ad9ad69bb38fca6a0",
|
|
70
70
|
"tests/nav-link.test.js": "d36e33ad7483abca27ae6e2098ad2f75bafa18d91f3beba70372ebe6b9b88994",
|
|
71
71
|
"tests/notices.test.js": "2b02aa84e780dc4c82e6c503fc8fe17c64a63bee0709627866ef97c99cb4e131",
|
|
72
|
-
"tests/public.test.js": "
|
|
72
|
+
"tests/public.test.js": "cd4cc53da9709d400d082ef6df01b3f49e6fc419e4b020b588464306a5d907e9"
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"booking": {
|
|
76
|
-
"version": "1.6.
|
|
76
|
+
"version": "1.6.3",
|
|
77
77
|
"files": {
|
|
78
|
-
"CLAUDE.md": "
|
|
78
|
+
"CLAUDE.md": "1709e58b31a3ac7c1cbadbe6d4f8b0a01cf696b72981ccfa14cd1861133fac85",
|
|
79
79
|
"admin/css/index.css": "4f7731921c0c5038cb1566a6ab2d43ffc35bf9b161e58f87a0cff423e81b47fa",
|
|
80
80
|
"admin/lib/details.js": "8b37536bfa9ccfcc5265cc957ba8266d3f5f777133e7c0946ae49540f1c80e3c",
|
|
81
81
|
"admin/lib/diary-layout.js": "d818c79001fb9a9ecc15c96239ba961b0a1d98b9e8fce903eefb57d85adf2a73",
|
|
@@ -93,14 +93,14 @@
|
|
|
93
93
|
"collections/booking-bookings.json": "fa0b15cc81b4a2114734a0bc8adc4b4d25049d16cf62549b83634cdbeb543b52",
|
|
94
94
|
"collections/booking-services.json": "cecb500a9215a0eba894e6b4c9ae78f5fbeb209830d07d47050c56c75143f66a",
|
|
95
95
|
"config.js": "5565fd627481cf809322257cc300a7cbb7ccbf6cae076b4cf8a7453fcf554bd3",
|
|
96
|
-
"docs/guide.md": "
|
|
96
|
+
"docs/guide.md": "8f5898627abb1b21f90809b4f2e4c29f80d478bd11c0f1aec5319cddc40d14f9",
|
|
97
97
|
"inject/body.html": "ab55ce945eec2302eb0c836a4626cffa5e091d050c6a59c6f2af332b05570777",
|
|
98
98
|
"lib/call.js": "87c11b8531e1a6d247d52d4e03ea8e5aa06a1af49cbd7cbd0af97e2586237f7b",
|
|
99
99
|
"lib/extensions.js": "8cec28ff8685daad8083b23f034f1c3bf82fd2322943dd19d46570ccdc311105",
|
|
100
100
|
"lib/mail.js": "cad8f409df62d6e42f0f4024ec364bbe018a62b842fa3f7c38786fd4e7f92408",
|
|
101
101
|
"lib/store.js": "7aa7967e4694e5a3b85378f60302e5a47394b1d90c7dd86e09578156f1c13e03",
|
|
102
|
-
"plugin.js": "
|
|
103
|
-
"plugin.json": "
|
|
102
|
+
"plugin.js": "4a303855a5a7f746e07e45a937dfeac8984dc9254c2f9b1a60b6f6306912ed84",
|
|
103
|
+
"plugin.json": "d099b136335028b2bca2bd21065c6ae343099f71635f9c793c95567a6e8a63db",
|
|
104
104
|
"plugin.public.js": "4d5b4e8ad6943182ee3b6c6e6f6762a2d3f676d8d7ee363fcb07bd47d3d6532e",
|
|
105
105
|
"public/booking.css": "8b88989fdd04e50efc1688a2f32fe2e422299d7d13b81bb8fce0cc162fa99df6",
|
|
106
106
|
"public/booking.js": "7dfc3d205696967013886567f67a867443c2e370bac29019d69911d695fdbade",
|
|
@@ -110,42 +110,42 @@
|
|
|
110
110
|
"public/lib/time.js": "48e13a23e3d7a4916e2097e8f87c03a87d37b71c4ef037a5a16143c6a4185c41",
|
|
111
111
|
"templates/booking.html": "a4995de46dfdab7e692b56f21347f29c76320cf3b0477889dc3c5d14e0b6dcd7",
|
|
112
112
|
"tests/admin-diary.test.js": "4e34b447d62b41272ceec49bad9d491ae00dd04f4760d8a7a3046be1912f4abe",
|
|
113
|
-
"tests/api.test.js": "
|
|
113
|
+
"tests/api.test.js": "efb3767f7efde08c11c8edcb5753889773b421188798f725aff0e4e533860aff",
|
|
114
114
|
"tests/public.test.js": "0b7b626d60df5a3d73d7c0077b73f59b62a9fded98ce84a8d93cd32cd0333159",
|
|
115
115
|
"tests/pure.test.js": "8ba0c8f76b4c9efcc422f39ff29509612544d81f760025a4b9a5594af14e7bcf"
|
|
116
116
|
}
|
|
117
117
|
},
|
|
118
118
|
"feedback": {
|
|
119
|
-
"version": "1.1.
|
|
119
|
+
"version": "1.1.4",
|
|
120
120
|
"files": {
|
|
121
|
-
"CLAUDE.md": "
|
|
121
|
+
"CLAUDE.md": "af921af5c262cac7ac784fa86b780e4f31dac1baa510b4b3fd32a52521d4cccf",
|
|
122
122
|
"admin/css/index.css": "f2e934c8b94f883340148aba17945dfa725eeb2b0ddb6cabb8a8c17e2487c1de",
|
|
123
123
|
"admin/lib/kit.js": "913761a2cc19b69bed3f05267d45fdb1c08338b1edb081b9272231733d9b5790",
|
|
124
124
|
"admin/templates/feedback.html": "a366dce78f6c3e84a7cb0ac63eba53e1980a4eb44154496a0536f5214c5bb36f",
|
|
125
|
-
"admin/views/feedback.js": "
|
|
125
|
+
"admin/views/feedback.js": "768d76da473dc4905e8e8e9133a715c1db8c3cb53d91756b5c7d067a92971463",
|
|
126
126
|
"admin/views/inbox.js": "649b86844230be73939d82aa7c97432252c0b119baa102ebf53808e1d5f23737",
|
|
127
127
|
"admin/views/send.js": "5868038a4020158746c5705d2a0ab5f552ac5021328a1dcf64a2def890495427",
|
|
128
128
|
"collections/feedback-reports.json": "a072291b52463f7785a77f5f77d36b8238ba8f89cfc2672c6642eccdb3144486",
|
|
129
129
|
"config.js": "29d9999d1028c2c11931f67fb9abdf35132430b7cf58c161681e734c0b45143a",
|
|
130
130
|
"docs/guide.md": "e006cc3f3c114583285964bc1b5d22b2a01285c2d2015d132434bfed363917d4",
|
|
131
|
-
"lib/receiver.js": "
|
|
131
|
+
"lib/receiver.js": "d77962c895617f9adf2fb16d29992fd83b75a3419a4aa71ac3e3bbe8c2089839",
|
|
132
132
|
"lib/sender.js": "037f9b4469a34ffe1d302a633e664860d625f7fb3be4dfadd01b8a3d094849b7",
|
|
133
133
|
"lib/shape.js": "67acf62f812c2c9a01b52776641ed7ae1601c59ebab8ea278a07d1087a9f299b",
|
|
134
134
|
"plugin.js": "c388bbc2a49b5902773b15a74379c40eeaea58396058136e7901e405b3a396f8",
|
|
135
|
-
"plugin.json": "
|
|
135
|
+
"plugin.json": "612357f1b3166f5bad11a563d0e7759c262a2328eba9890c5937dc98d7752363",
|
|
136
136
|
"tests/api.test.js": "3fd27c561af593168ff0c2d717900feda89da4e306f8e0ecac0a6ad68c6d3359",
|
|
137
137
|
"tests/shape.test.js": "5133578142a79cc1a100b3215436372129f01fb0de8c757fd743ff99e1d7e37f"
|
|
138
138
|
}
|
|
139
139
|
},
|
|
140
140
|
"mail-reader": {
|
|
141
|
-
"version": "1.2.
|
|
141
|
+
"version": "1.2.7",
|
|
142
142
|
"files": {
|
|
143
|
-
"CLAUDE.md": "
|
|
143
|
+
"CLAUDE.md": "6c1fe7956f0d2cc637bff3441061d083e6d938ccf58045473ec49c8fb38f41c1",
|
|
144
144
|
"admin/views/mail.js": "c5c6483f9618c75cac8f4be32d3aea99511db76d4b9d38b8ef43bdc1d35db3c5",
|
|
145
145
|
"config.js": "6e65803ad822a9acda40c203631b8eb32a87cf9d9e5f1c7cb59d01aa406075c6",
|
|
146
|
-
"docs/guide.md": "
|
|
146
|
+
"docs/guide.md": "d6b5944740ea1bbd689034ef726d8bc0f9bfeb28ea62b63c258292962d45cf18",
|
|
147
147
|
"plugin.js": "7f602a60c4eca7d3637659d03d5de9e242584666846b16c9e5a1b758e4856c9f",
|
|
148
|
-
"plugin.json": "
|
|
148
|
+
"plugin.json": "05a5fa1e0ff69eb2cfe0c07a1abbe2cb4a1cd57a75511d0ae6fb1a74524a65e4"
|
|
149
149
|
}
|
|
150
150
|
},
|
|
151
151
|
"security": {
|
|
@@ -175,9 +175,9 @@
|
|
|
175
175
|
}
|
|
176
176
|
},
|
|
177
177
|
"shopping-cart": {
|
|
178
|
-
"version": "1.6.
|
|
178
|
+
"version": "1.6.1",
|
|
179
179
|
"files": {
|
|
180
|
-
"CLAUDE.md": "
|
|
180
|
+
"CLAUDE.md": "2759884523ebf0f9323cdefdd220fc9c0ad952f9964c065f19b97530e61f7533",
|
|
181
181
|
"admin/css/index.css": "dfe1ffc05b9161af3d8a74e82bd36c46991cb3b0409223b213eb8eca6fba9d55",
|
|
182
182
|
"admin/lib/chart.js": "b01ce5cfac773c8df8efccab50b67d3dfa99fbead0037d817553db4a6688d4d6",
|
|
183
183
|
"admin/lib/kit.js": "d9aa90469b3f589beaff0c3fb6ad3d983ab3fd6d94c06965be639a665bfe052f",
|
|
@@ -191,22 +191,22 @@
|
|
|
191
191
|
"collections/shop-orders.json": "0bca8eef2d5f9108651f73d273480265c167dc983aa115ce59613d13015ca255",
|
|
192
192
|
"collections/shop-products.json": "4d4613ecec7b8a5a0744c4f0a57b17a4429b461e060206a4bbdea5ad7c8c75dc",
|
|
193
193
|
"config.js": "31b336560d21a802d799ed487d5c3116de898446ba3521068b10d192f4ebbb2d",
|
|
194
|
-
"docs/guide.md": "
|
|
194
|
+
"docs/guide.md": "5b63bd03b015e273c2880ac431a9faccdcc389e8c5f367697aeb4502be8e280b",
|
|
195
195
|
"inject/body.html": "7e7e5b408f47abb8650e0a7d5e277daf8a634f4c6877c5259f837b1bf3d6c8e8",
|
|
196
|
-
"lib/accounts.js": "
|
|
196
|
+
"lib/accounts.js": "5115cec6a145da18c106dfdbf5fd0b2a1bad0c68e8fac4ef623287e85db75e5c",
|
|
197
197
|
"lib/commission.js": "5b10475d98cff2aaf4c76b9950f8d698d6262e36754a8963669690a8e3e9bd52",
|
|
198
198
|
"lib/extensions.js": "73d3515b52c066e3e616c425d91a7a48a6aab153e7d14ae7278b38ee32649352",
|
|
199
|
-
"lib/payments.js": "
|
|
199
|
+
"lib/payments.js": "de093035ce732cd4f7f569fcd32cb5a86e68596a3ae54ec7d00e949b3024f7a7",
|
|
200
200
|
"lib/pricing.js": "5644f2dd55fc3cfd7f40b0169dea5fb547e8b302239b8c17f23cfa177e42eab5",
|
|
201
201
|
"lib/render.js": "2a2b9ab4a24728186e1e45e6429c7406a6d5ca3d4b43db1758bbd54405d105b7",
|
|
202
202
|
"lib/sales.js": "16804f0ba4e61e8a2887a7c4172da37533850a1cd975a177b754f3350268e946",
|
|
203
203
|
"lib/samples.js": "912b3d5bd543dffa5f870caf3c2b873f642fd55e3ebf3f97e1b31eeaaac77d3b",
|
|
204
|
-
"lib/shop.js": "
|
|
204
|
+
"lib/shop.js": "190ad53acffb88ca9462900091918ea5d3e40fb666e89b9c060832035284115c",
|
|
205
205
|
"lib/summary.js": "cdb13ce3d1799d429059fa03f179281711dc00427ccf2eb6b8f8aa52fef26295",
|
|
206
|
-
"plugin.js": "
|
|
207
|
-
"plugin.json": "
|
|
206
|
+
"plugin.js": "f776e6bbdd98bf0b487e8247bc7334e78a1f8601a54b26b8c30df817619c2ebc",
|
|
207
|
+
"plugin.json": "abc448993e39b008d2f8d69deb55176bd090f5272003164a0931059dca4a5fc5",
|
|
208
208
|
"plugin.public.js": "2f92e8340c9da653401c11dd99dab033dc6772d63fa2114489192c48e9e0be00",
|
|
209
|
-
"public/account.js": "
|
|
209
|
+
"public/account.js": "5f644016c098c7879c59948d662f5997cb61680996204ecb7897baab8945d434",
|
|
210
210
|
"public/cart.js": "7338141392ec653ff616eb75688d2a2ad08cff8427d7020d381f540be0037b73",
|
|
211
211
|
"public/lib/money.js": "d22b2b6f80dde6f7887b69e24188c05ea93c02217aba639b5331aa3bd616f4fc",
|
|
212
212
|
"public/lib/orders.js": "09478e20d238adc8e387b3d9efcb13c441898fab18387ce6a1a8a92304ac4c5b",
|
|
@@ -227,31 +227,32 @@
|
|
|
227
227
|
"public/shop.css": "c01603589727c91ff4204222e594b3f52e3ca165c1ebf9e6cef6c9f006f6e57b",
|
|
228
228
|
"roles/shop-customer.json": "3f3366c536c9fbf2b2ce146bb3090b008d5fbfe22752e764eb8e5a1de9cd611c",
|
|
229
229
|
"templates/shop.html": "4649e57d23458833a157799dc80141824eb0f658d96734bd1859c68e0ba9a8b9",
|
|
230
|
-
"tests/accounts.test.js": "
|
|
231
|
-
"tests/api.test.js": "
|
|
230
|
+
"tests/accounts.test.js": "e64adc2a855d4ca017682bba7f75b052534f826556d8d35d825fb79cbc66596a",
|
|
231
|
+
"tests/api.test.js": "8bccf43e520bca7bb73837a74b3c3cf1fdb3a8db8fb5026a27bc0e5ed9d5ca80",
|
|
232
232
|
"tests/commission.test.js": "1dfb26a90d3dfcf9f57f2174ac5d9b2310570b21f82c79bf9a2011e7f4911e85",
|
|
233
|
-
"tests/pure.test.js": "
|
|
233
|
+
"tests/pure.test.js": "13620f708f21e2590979e8579e2e7788d6109e1682bf73d7b1eb64838da35196",
|
|
234
234
|
"tests/sales.test.js": "9025bc608b04bcc14f5bb2c98b4f7c35de99f3ddab4ffa59772d045795bf2cb7"
|
|
235
235
|
}
|
|
236
236
|
},
|
|
237
237
|
"support": {
|
|
238
|
-
"version": "1.1.
|
|
238
|
+
"version": "1.1.2",
|
|
239
239
|
"files": {
|
|
240
|
-
"CLAUDE.md": "
|
|
240
|
+
"CLAUDE.md": "9b35a7df9335e69278c261f3e68944a4fea87c495c2ff500c14990dc8a6c7337",
|
|
241
241
|
"admin/css/index.css": "bad02a487ebb131b64ee55e2a4533c8b79d6ada80fffd34b0952bf3b68204a36",
|
|
242
242
|
"admin/lib/kit.js": "22f02768625bcad9d9c8e64bf0a8fe95f7c5ff6dcf8abb189df8203362a7a4e1",
|
|
243
243
|
"admin/templates/support.html": "2bb69d0443a367a402e6160fe68f4af3e8983b578cfff9575b4df9a534fcb480",
|
|
244
244
|
"admin/views/desk.js": "ac043630ad08c8f2053f3f908be18586d9a4798976d20427cf5215ee042c807c",
|
|
245
245
|
"admin/views/inbox.js": "f570595d7b2c831d151e0aaf85ff903e6e2b7066124b3f9d23c2c24f5b1734cf",
|
|
246
|
-
"admin/views/support.js": "
|
|
246
|
+
"admin/views/support.js": "4be6cce46067ee7af140f43253da36e060add2a9190e91f013fd1aad668fcd7b",
|
|
247
247
|
"collections/support-tickets.json": "55a0856bacd66c2e8f1e3c85bdf3638e5ff826560943cda854a8f2d336a88905",
|
|
248
248
|
"config.js": "eb7ccc1169fa8b2be52a213536825492fedaec5fa73fd749cba9f498dd54b503",
|
|
249
|
+
"docs/guide.md": "d0d38dacbdf68bfaba1ed4e62c8d182372a683d255a8ff75ca57bd0be2a0b082",
|
|
249
250
|
"lib/mail.js": "d14aae3a59aad6af61451863cdaaf94fc4a87810b16e1dcb9c655dda6c803b3b",
|
|
250
|
-
"lib/receiver.js": "
|
|
251
|
+
"lib/receiver.js": "64c292281bde5a942de4d04a16911d0730a4da7e80ce6d91a9da864b9f886b59",
|
|
251
252
|
"lib/sender.js": "b20aaee34ade89814a897f68467f9902693843fa6b5c8eb0090c4059193c8227",
|
|
252
253
|
"lib/shape.js": "04d840b589d20700ccb2cfa4c72072f8d9f8e03f0573e2ea1177a7a83a383bea",
|
|
253
254
|
"plugin.js": "e3c50a05fa99cb24375782bbb3ca8c58cf5ddc3036937e6115f4d963715c120e",
|
|
254
|
-
"plugin.json": "
|
|
255
|
+
"plugin.json": "c45819016588e7e26426830ce30fd6d7561b61ffcac2a8ad48bb4c178c138188",
|
|
255
256
|
"tests/api.test.js": "1fa87f6ae12c48fc34a5fb2b5bbeed6cfaa36329610674d4fae05d64734dbb15",
|
|
256
257
|
"tests/shape.test.js": "364c6b7680699f59fcd449cd448184486488838300f155135daea4c05387cf74"
|
|
257
258
|
}
|
|
@@ -108,3 +108,9 @@ Right-click on the open message is engine code (`_lib/admin/mail/body-menu.js`,
|
|
|
108
108
|
The page-size picker under the list is engine code (`listPageSize()`/`pageAfterResize()` in
|
|
109
109
|
`_lib/admin/mail/settings-section.js`, sent as `limit` by `reader-view.js`, capped by `maxListLimit`);
|
|
110
110
|
this version only documents it in docs/guide.md. 0.116.8 is already released, so it can ship now.
|
|
111
|
+
|
|
112
|
+
## Private hosts refused (guide only, 1.2.7, 2026-10-13; engine `_lib/mail/hostGuard.js`)
|
|
113
|
+
|
|
114
|
+
Saving or testing a mailbox on a private, loopback or link-local address is refused by the shared read routes
|
|
115
|
+
unless it is the site's own outgoing server or `allowPrivateMailHosts` is set; `/accounts/test` is limited to
|
|
116
|
+
10 a minute. Engine code - this version only documents the option in docs/guide.md. Ship with the matching core.
|
|
@@ -99,6 +99,7 @@ Whoever runs the site can change these in the plugin's configuration file (`conf
|
|
|
99
99
|
| `listLimit` | 50 | Messages per page until you pick another size under the list |
|
|
100
100
|
| `allowRemoteImages` | false | Load remote images without asking |
|
|
101
101
|
| `allowInsecureTLS` | false | Accept mail server certificates that do not verify (see Tips) |
|
|
102
|
+
| `allowPrivateMailHosts` | false | Let a mailbox be on the site's own private network (a LAN or loopback address). Off, a mailbox on such an address is refused; the site's own outgoing server is always allowed |
|
|
102
103
|
| `connectionTimeoutMs` | 20000 | How long to wait for a mail server, in milliseconds |
|
|
103
104
|
| `maxConnections` | 20 | Most mail server connections kept open at once |
|
|
104
105
|
| `maxMessageBytes` | 25000000 | Largest message that will be opened, in bytes |
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mail-reader",
|
|
3
3
|
"displayName": "Mail Reader",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.7",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Read-only IMAP mailbox reader. Connects to a standard IMAP server and reads mail without ever writing to it.",
|
|
7
7
|
"author": "Darryl Waterhouse",
|
|
8
|
-
"date": "2026-
|
|
8
|
+
"date": "2026-10-13",
|
|
9
9
|
"icon": "mail",
|
|
10
10
|
"uses": {
|
|
11
11
|
"contacts": "Add sender to contacts"
|
|
@@ -94,7 +94,8 @@ A cached page can show a stale stock flag until it is re-rendered; checkout re-p
|
|
|
94
94
|
- Storefront CSS tokens are defined on `body`, not `:root`: themes set --dm-* on body's dm-theme-* class.
|
|
95
95
|
- The site's own input/link styles reach the storefront: see the "reach in here" block at the end of shop.css.
|
|
96
96
|
- `siteBase()` uses `request.host` (with the port); `request.hostname` drops it under Fastify 5.
|
|
97
|
-
- A test site on a non-standard port needs `site.baseUrl` for Stripe/PayPal return addresses behind a proxy
|
|
97
|
+
- A test site on a non-standard port needs `site.baseUrl` for Stripe/PayPal return addresses behind a proxy, and
|
|
98
|
+
emails have no links to the order until it (or a real sign-in) gives the site a trusted address (1.6.1).
|
|
98
99
|
- Admin views: item templates live in JS (a templateUrl goes through _.render); checkboxes are set after render
|
|
99
100
|
and listened to; slideovers via kit.openPanel (disposed and removed on close).
|
|
100
101
|
|
|
@@ -201,3 +202,18 @@ Cart only READS it (`lib/commission.js currentCommission`, re-read when the file
|
|
|
201
202
|
- Never shown to the customer (`publicOrder` leaves it out). Shown to the shop: Sales (commission, returned, after
|
|
202
203
|
commission; a CSV column), the order, Shop settings › Payment ("set by your provider" - read-only).
|
|
203
204
|
- The manager's statements add up what the orders carry - nothing is recalculated there.
|
|
205
|
+
|
|
206
|
+
## 1.6.1 (2026-10-13): no Host headers in email links; confirm says nothing; refunds under the lock
|
|
207
|
+
|
|
208
|
+
- `siteBase(request)` is for a payment provider's return addresses only (Site URL, else the shopper's own origin) and
|
|
209
|
+
remembers nothing: a forged Host on one `/quote` used to become the address - with the receipt tokens - in every email
|
|
210
|
+
until the next request. Email links use `quietBase()` (async: Site URL, else core's `trustedOrigin` - an address someone
|
|
211
|
+
has really signed in on, as password resets do), exposed to extensions as `services.linkBase()`; `services.siteBase()`
|
|
212
|
+
stays sync and is the Site URL or ''. `receiptUrl` is '' when there is none and the log says so once; `startCheckout`
|
|
213
|
+
prefers the Site URL to the extension's `base`. Account confirm and forgotten-password links use the same rule for the
|
|
214
|
+
request (`linkBase(request)`); with no trusted address, register answers 409 and forgot sends nothing.
|
|
215
|
+
- `POST /account/confirm` needs the token (shape-checked) and a waiting account, answers `{confirmed: true}` only, and
|
|
216
|
+
is off when accounts are: user ids are public (`createdBy`), so "already confirmed -> here is the email" leaked emails.
|
|
217
|
+
- `refundOrder` runs under `withLock` (provider call and `moveOrderNow` in one), and Stripe refunds carry
|
|
218
|
+
`Idempotency-Key: shopping-cart-refund-<orderId>` (sessions: `shop-session-<orderId>`) so a retry after a timeout
|
|
219
|
+
returns the first result.
|
|
@@ -23,7 +23,7 @@ The free Shopping Cart sends no emails at all - not to the shopper and not to yo
|
|
|
23
23
|
2. Click **Add a product**, or **or try samples** to load six sample products with pictures, options and stock. You can edit or delete the samples later.
|
|
24
24
|
3. Click **Payment settings** and turn on at least one way to pay (see Settings below). Pay offline is on by default.
|
|
25
25
|
4. Click **See the shop** to check it, then link to `/shop` from your menu or put `[shop-products /]` on any page.
|
|
26
|
-
5. If your site sits behind a proxy or on an unusual port, set your site's address first: [Site Settings](#/settings) > General > **Site URL** (for example `https://example.com`). Stripe and PayPal send shoppers back to this address after they pay. Without it the Shop guesses the address from the request, which can be wrong behind a proxy.
|
|
26
|
+
5. If your site sits behind a proxy or on an unusual port, set your site's address first: [Site Settings](#/settings) > General > **Site URL** (for example `https://example.com`). Stripe and PayPal send shoppers back to this address after they pay, and the links in order and account emails are built on it. Without it the Shop guesses the return address from the request, which can be wrong behind a proxy, and emails go out without their links until it is set.
|
|
27
27
|
|
|
28
28
|
## Screens
|
|
29
29
|
|
|
@@ -118,12 +118,17 @@ export async function register({name, email, password, phone = ''}, {link, shopT
|
|
|
118
118
|
return {sent: true};
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Click the link: the account is active. Returns the user, or throws. Only a waiting account with the right
|
|
123
|
+
* token is answered - never an already-active user: user ids are public (createdBy on entries, author fields),
|
|
124
|
+
* so answering "confirmed" for an active id would hand anyone that person's email.
|
|
125
|
+
*/
|
|
122
126
|
export async function confirm(userId, token) {
|
|
123
127
|
const user = /^[0-9a-f-]{36}$/i.test(String(userId || '')) ? await getUserById(String(userId)) : null;
|
|
124
128
|
const pending = user?.meta?.shop?.pending;
|
|
125
|
-
if (user
|
|
126
|
-
|
|
129
|
+
if (!user || user.isActive || !pending || !/^[0-9a-f]{48}$/i.test(String(token || ''))) {
|
|
130
|
+
throw bad(404, 'That link is not one we know - it may have been used already.');
|
|
131
|
+
}
|
|
127
132
|
const a = Buffer.from(sha(token));
|
|
128
133
|
const b = Buffer.from(String(pending.hash || ''));
|
|
129
134
|
if (a.length !== b.length || !timingSafeEqual(a, b)) throw bad(404, 'That link is not one we know - it may have been used already.');
|
|
@@ -64,12 +64,14 @@ export function stripeForm(obj, prefix = '', out = new URLSearchParams()) {
|
|
|
64
64
|
return out;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
/** `idempotencyKey`: Stripe answers a repeat of the same request (a retry after a timeout) with the first result. */
|
|
68
|
+
function stripeInit(secretKey, method = 'GET', form = null, idempotencyKey = '') {
|
|
68
69
|
return {
|
|
69
70
|
method,
|
|
70
71
|
headers: {
|
|
71
72
|
Authorization: `Bearer ${secretKey}`,
|
|
72
73
|
...(form ? {'Content-Type': 'application/x-www-form-urlencoded'} : {}),
|
|
74
|
+
...(idempotencyKey ? {'Idempotency-Key': String(idempotencyKey).slice(0, 255)} : {}),
|
|
73
75
|
'Stripe-Version': '2024-06-20'
|
|
74
76
|
},
|
|
75
77
|
...(form ? {body: form.toString()} : {})
|
|
@@ -128,7 +130,8 @@ export async function stripeCreateSession({secretKey, order, successUrl, cancelU
|
|
|
128
130
|
description: `${shopName ? `${shopName} ` : ''}order #${order.number}`.slice(0, 1000)},
|
|
129
131
|
line_items: Object.fromEntries(items.map((x, i) => [i, x]))
|
|
130
132
|
});
|
|
131
|
-
|
|
133
|
+
// One session per order: a retry of the same checkout gets the same page back, not a second session.
|
|
134
|
+
const s = await call(`${STRIPE}/checkout/sessions`, stripeInit(secretKey, 'POST', form, `shop-session-${order.id}`), 'Stripe');
|
|
132
135
|
if (!s?.url) throw new PaymentError('Stripe did not return a payment page.', {provider: 'Stripe'});
|
|
133
136
|
return {id: s.id, url: s.url};
|
|
134
137
|
}
|
|
@@ -150,9 +153,9 @@ export function stripeSessionPaid(session, order) {
|
|
|
150
153
|
return {ref: session.id, payment: typeof session.payment_intent === 'string' ? session.payment_intent : session.payment_intent?.id || ''};
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
export async function stripeRefund({secretKey, paymentIntent, amount}) {
|
|
156
|
+
export async function stripeRefund({secretKey, paymentIntent, amount, idempotencyKey = ''}) {
|
|
154
157
|
const form = stripeForm({payment_intent: paymentIntent, ...(amount ? {amount} : {})});
|
|
155
|
-
return call(`${STRIPE}/refunds`, stripeInit(secretKey, 'POST', form), 'Stripe');
|
|
158
|
+
return call(`${STRIPE}/refunds`, stripeInit(secretKey, 'POST', form, idempotencyKey), 'Stripe');
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
/** Can this key talk to Stripe? */
|