domma-cms 1.6.37 → 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/blog/CLAUDE.md +11 -1
- package/plugins/blog/lib/cache.js +43 -0
- package/plugins/blog/lib/page.js +28 -9
- package/plugins/blog/plugin.js +14 -13
- package/plugins/blog/plugin.json +1 -1
- package/plugins/blog/plugin.public.js +1 -1
- package/plugins/blog/tests/cache.test.js +65 -0
- package/plugins/booking/CLAUDE.md +14 -2
- package/plugins/booking/admin/lib/diary-layout.js +18 -0
- package/plugins/booking/admin/views/diary.js +153 -152
- package/plugins/booking/admin/views/people.js +64 -66
- package/plugins/booking/admin/views/settings.js +31 -32
- package/plugins/booking/plugin.json +1 -1
- package/plugins/booking/tests/admin-diary.test.js +9 -1
- package/plugins/feedback/CLAUDE.md +62 -0
- package/plugins/feedback/admin/css/index.css +16 -1
- package/plugins/feedback/admin/lib/kit.js +106 -0
- package/plugins/feedback/admin/lib/settings.js +107 -0
- package/plugins/feedback/admin/views/feedback.js +4 -1
- package/plugins/feedback/admin/views/inbox.js +11 -2
- package/plugins/feedback/admin/views/send.js +74 -7
- package/plugins/feedback/docs/guide.md +9 -9
- package/plugins/feedback/lib/attachments.js +133 -0
- package/plugins/feedback/lib/cache.js +43 -0
- package/plugins/feedback/lib/receiver.js +36 -6
- package/plugins/feedback/lib/sender.js +89 -6
- package/plugins/feedback/lib/shape.js +111 -6
- package/plugins/feedback/plugin.js +173 -17
- package/plugins/feedback/plugin.json +2 -2
- package/plugins/feedback/tests/api.test.js +125 -3
- package/plugins/feedback/tests/settings.test.js +59 -0
- package/plugins/feedback/tests/shape.test.js +87 -1
- package/plugins/free-tier.lock.json +74 -62
- package/plugins/mail-reader/CLAUDE.md +15 -4
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/shopping-cart/CLAUDE.md +35 -2
- package/plugins/shopping-cart/admin/css/index.css +3 -0
- package/plugins/shopping-cart/admin/views/orders.js +75 -23
- package/plugins/shopping-cart/admin/views/shop.js +1 -1
- package/plugins/shopping-cart/docs/guide.md +7 -3
- package/plugins/shopping-cart/lib/payments.js +15 -2
- package/plugins/shopping-cart/lib/refunds.js +51 -0
- package/plugins/shopping-cart/lib/sales.js +15 -8
- package/plugins/shopping-cart/lib/shop.js +38 -3
- package/plugins/shopping-cart/plugin.js +47 -19
- package/plugins/shopping-cart/plugin.json +2 -2
- package/plugins/shopping-cart/tests/api.test.js +52 -0
- package/plugins/shopping-cart/tests/commission.test.js +1 -1
- package/plugins/shopping-cart/tests/pure.test.js +16 -0
- package/plugins/shopping-cart/tests/refunds.test.js +42 -0
- package/plugins/support/CLAUDE.md +58 -0
- package/plugins/support/admin/css/index.css +16 -1
- package/plugins/support/admin/lib/kit.js +192 -8
- package/plugins/support/admin/lib/settings.js +123 -0
- package/plugins/support/admin/views/desk.js +55 -12
- package/plugins/support/admin/views/inbox.js +17 -4
- package/plugins/support/admin/views/support.js +4 -1
- package/plugins/support/docs/guide.md +7 -9
- package/plugins/support/lib/attachments.js +133 -0
- package/plugins/support/lib/cache.js +43 -0
- package/plugins/support/lib/receiver.js +45 -10
- package/plugins/support/lib/sender.js +96 -10
- package/plugins/support/lib/shape.js +153 -12
- package/plugins/support/plugin.js +159 -29
- package/plugins/support/plugin.json +2 -2
- package/plugins/support/tests/api.test.js +125 -3
- package/plugins/support/tests/settings.test.js +68 -0
- package/plugins/support/tests/shape.test.js +76 -2
- package/server/routes/outreach-public.js +2 -2
- package/server/services/request-log.js +3 -1
|
@@ -11,6 +11,7 @@ import os from 'node:os';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import Fastify from 'fastify';
|
|
13
13
|
import feedback from '../plugin.js';
|
|
14
|
+
import {allReports, forget} from '../lib/receiver.js';
|
|
14
15
|
import {getRoleHierarchy, getRoleLevel, load as loadRoles} from '../../../server/services/roles.js';
|
|
15
16
|
|
|
16
17
|
const PLUGINS = path.resolve('config/plugins.json');
|
|
@@ -20,6 +21,8 @@ let savedPlugins;
|
|
|
20
21
|
let manager;
|
|
21
22
|
let dataDir;
|
|
22
23
|
let managerUp = true;
|
|
24
|
+
// The manager's settings as stored; the plugin reads them per request (1.1.7).
|
|
25
|
+
const managerSettings = {perSiteMax: 6, perSiteMinutes: 10};
|
|
23
26
|
const notices = [];
|
|
24
27
|
const siteNotices = [];
|
|
25
28
|
// Roles are data: the level-0 role and one below it, whatever this checkout calls them.
|
|
@@ -38,7 +41,7 @@ async function site({token, user}) {
|
|
|
38
41
|
const app = Fastify();
|
|
39
42
|
await app.register(feedback, {
|
|
40
43
|
auth: {authenticate: async (req) => { req.user = user; }, requirePermission: () => async () => {}},
|
|
41
|
-
settings: {}, mode: 'sender', timer: false, dataDir: await fs.mkdtemp(path.join(dataDir, 'site-')),
|
|
44
|
+
settings: {}, readSettings: async () => ({}), mode: 'sender', timer: false, dataDir: await fs.mkdtemp(path.join(dataDir, 'site-')),
|
|
42
45
|
env: {MANAGER_URL: 'http://127.0.0.1:8192/', FLEET_TOKEN: token, SITE_SLUG: TOKENS[token] || 'x'},
|
|
43
46
|
fetch: fetchInto(manager),
|
|
44
47
|
hooks: {notify: (n) => siteNotices.push(n), registerNotificationSource() {}}
|
|
@@ -66,7 +69,7 @@ before(async () => {
|
|
|
66
69
|
auth: {authenticate: async (req) => { req.user = req.headers['x-test-user'] ? JSON.parse(req.headers['x-test-user']) : {id: 'v1', name: 'Vendor', role: TOP}; },
|
|
67
70
|
requirePermission: () => async () => {}},
|
|
68
71
|
prefix: '/api/plugins/feedback', dataDir: await fs.mkdtemp(path.join(dataDir, 'manager-')),
|
|
69
|
-
settings: {perSiteMax: 6, perSiteMinutes: 10}, mode: 'receiver',
|
|
72
|
+
settings: {perSiteMax: 6, perSiteMinutes: 10}, readSettings: async () => ({...managerSettings}), mode: 'receiver',
|
|
70
73
|
slugFromToken: async (h) => TOKENS[String(h || '').replace(/^Bearer\s+/i, '')] || null,
|
|
71
74
|
siteNames: () => ({acme: 'Acme Ltd', beta: 'Beta Co'}),
|
|
72
75
|
hooks: {notify: (n) => notices.push(n), registerNotificationSource() {}}
|
|
@@ -137,6 +140,13 @@ test('the per-site throttle and the body limit', async () => {
|
|
|
137
140
|
assert.equal((await call(manager, 'POST', '/api/plugins/feedback/fleet/submit', {title: 'x', description: 'y'}, h)).status, 429);
|
|
138
141
|
assert.equal((await call(manager, 'POST', '/api/plugins/feedback/fleet/submit', {title: 'x', description: 'y'},
|
|
139
142
|
{authorization: 'Bearer tok-beta'})).status, 201, 'another site is unaffected');
|
|
143
|
+
// A higher limit saved in the cog applies to the next report - no restart.
|
|
144
|
+
managerSettings.perSiteMax = 8;
|
|
145
|
+
for (let i = 0; i < 2; i++) {
|
|
146
|
+
assert.equal((await call(manager, 'POST', '/api/plugins/feedback/fleet/submit', {title: `m${i}`, description: 'y'}, h)).status, 201, 'live limit');
|
|
147
|
+
}
|
|
148
|
+
assert.equal((await call(manager, 'POST', '/api/plugins/feedback/fleet/submit', {title: 'x', description: 'y'}, h)).status, 429);
|
|
149
|
+
managerSettings.perSiteMax = 6;
|
|
140
150
|
const big = await manager.inject({method: 'POST', url: '/api/plugins/feedback/fleet/submit',
|
|
141
151
|
headers: {'content-type': 'application/json', authorization: 'Bearer tok-acme'},
|
|
142
152
|
payload: JSON.stringify({title: 't', description: 'z'.repeat(40 * 1024)})});
|
|
@@ -240,7 +250,7 @@ test('a site the manager did not start cannot send, and says so', async () => {
|
|
|
240
250
|
const app = Fastify();
|
|
241
251
|
await app.register(feedback, {
|
|
242
252
|
auth: {authenticate: async (req) => { req.user = {id: 'u', name: 'U'}; }, requirePermission: () => async () => {}},
|
|
243
|
-
settings: {}, mode: 'sender', timer: false, env: {}, dataDir: await fs.mkdtemp(path.join(dataDir, 'lone-')), hooks: {}
|
|
253
|
+
settings: {}, readSettings: async () => ({}), mode: 'sender', timer: false, env: {}, dataDir: await fs.mkdtemp(path.join(dataDir, 'lone-')), hooks: {}
|
|
244
254
|
});
|
|
245
255
|
assert.equal((await call(app, 'GET', '/mode')).body.connected, false);
|
|
246
256
|
assert.equal((await call(app, 'POST', '/send', {title: 't', description: 'd'})).status, 409);
|
|
@@ -309,3 +319,115 @@ test('the manager\'s level-0 role files its own feedback straight into the inbox
|
|
|
309
319
|
// No site token can speak as the manager.
|
|
310
320
|
assert.equal((await call(manager, 'GET', `${P}/fleet/mine`, null, {authorization: 'Bearer _manager'})).status, 401);
|
|
311
321
|
});
|
|
322
|
+
|
|
323
|
+
test('the inbox memory: polls share one read, and every write is seen by the very next read', async () => {
|
|
324
|
+
const P = '/api/plugins/feedback';
|
|
325
|
+
const acme = await site({token: 'tok-acme', user: {id: 'u1', name: 'Sam', email: 'sam@acme.test', role: 'admin'}});
|
|
326
|
+
forget();
|
|
327
|
+
const first = await allReports();
|
|
328
|
+
assert.equal(await allReports(), first, 'a second read inside the window is the same read');
|
|
329
|
+
await call(manager, 'GET', `${P}/count`);
|
|
330
|
+
// A site's report arrives: the badge and the inbox show it at once, not in 30 s.
|
|
331
|
+
const sent = await call(acme, 'POST', '/send', {type: 'idea', title: 'Cache me', description: 'A fresh read, please.'});
|
|
332
|
+
assert.equal(sent.status, 201);
|
|
333
|
+
assert.notEqual(await allReports(), first, 'the write retired the memory');
|
|
334
|
+
const inbox = (await call(manager, 'GET', `${P}/reports`)).body.reports;
|
|
335
|
+
const got = inbox.find(r => r.title === 'Cache me');
|
|
336
|
+
assert.ok(got, 'the new report is in the inbox straight away');
|
|
337
|
+
assert.ok((await call(manager, 'GET', `${P}/count`)).body.details.items.some(i => i.text === 'Cache me'), 'and on the badge');
|
|
338
|
+
// The manager's own change is visible on the next list, and a delete leaves no ghost.
|
|
339
|
+
assert.equal((await call(manager, 'PUT', `${P}/reports/${got.id}`, {status: 'acknowledged'})).status, 200);
|
|
340
|
+
assert.equal((await call(manager, 'GET', `${P}/reports`)).body.reports.find(r => r.id === got.id).status, 'acknowledged');
|
|
341
|
+
assert.equal((await call(manager, 'DELETE', `${P}/reports/${got.id}`)).status, 200);
|
|
342
|
+
assert.ok(!(await call(manager, 'GET', `${P}/reports`)).body.reports.some(r => r.id === got.id), 'gone from the very next read');
|
|
343
|
+
assert.ok(!(await call(acme, 'GET', '/sent')).body.reports.some(r => r.title === 'Cache me' && !r.local), 'and from the site\'s view');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ---- Attachments (1.1.6) ----
|
|
347
|
+
|
|
348
|
+
import multipart from '@fastify/multipart';
|
|
349
|
+
|
|
350
|
+
/** One file as a multipart form. */
|
|
351
|
+
function form(name, type, content) {
|
|
352
|
+
const b = '----fb' + Math.random().toString(16).slice(2);
|
|
353
|
+
return {
|
|
354
|
+
headers: {'content-type': `multipart/form-data; boundary=${b}`},
|
|
355
|
+
payload: Buffer.concat([Buffer.from(`--${b}\r\nContent-Disposition: form-data; name="file"; filename="${name}"\r\nContent-Type: ${type}\r\n\r\n`),
|
|
356
|
+
Buffer.from(content), Buffer.from(`\r\n--${b}--\r\n`)])
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** A site app that can take uploads (the engine registers multipart for every plugin). */
|
|
361
|
+
async function siteWithUploads(token, user) {
|
|
362
|
+
const app = Fastify();
|
|
363
|
+
await app.register(multipart, {limits: {fileSize: 10 * 1024 * 1024}});
|
|
364
|
+
await app.register(feedback, {
|
|
365
|
+
auth: {authenticate: async (req) => { req.user = user; }, requirePermission: () => async () => {}},
|
|
366
|
+
settings: {}, readSettings: async () => ({}), mode: 'sender', timer: false, dataDir: await fs.mkdtemp(path.join(dataDir, 'site-')),
|
|
367
|
+
env: {MANAGER_URL: 'http://127.0.0.1:8192/', FLEET_TOKEN: token, SITE_SLUG: TOKENS[token]},
|
|
368
|
+
fetch: fetchInto(manager), hooks: {}
|
|
369
|
+
});
|
|
370
|
+
return app;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
test('a screenshot travels with its report: uploaded on the site, sent after it, opened in the inbox', async () => {
|
|
374
|
+
const P = '/api/plugins/feedback';
|
|
375
|
+
const acme = await siteWithUploads('tok-acme', {id: 'u1', name: 'Sam', email: 'sam@acme.test'});
|
|
376
|
+
const png = Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47]), Buffer.alloc(3000, 7)]);
|
|
377
|
+
const up = await acme.inject({method: 'POST', url: '/attachments', ...form('C:\\Users\\sam\\shot.png', 'image/png', png)});
|
|
378
|
+
assert.equal(up.statusCode, 201, up.body);
|
|
379
|
+
const a = up.json();
|
|
380
|
+
assert.equal(a.name, 'shot.png', 'the last path segment only');
|
|
381
|
+
assert.equal(a.size, png.length);
|
|
382
|
+
|
|
383
|
+
// Refused: a type that can carry script, and an id this site never made.
|
|
384
|
+
assert.equal((await acme.inject({method: 'POST', url: '/attachments', ...form('x.svg', 'image/svg+xml', '<svg onload=alert(1)>')})).statusCode, 400);
|
|
385
|
+
assert.equal((await call(acme, 'POST', '/send', {title: 't', description: 'd', attachments: [{...a, id: '0f8fad5b-d9cb-469f-a165-000000000000'}]})).status, 400);
|
|
386
|
+
|
|
387
|
+
// Sent: the report says what is coming, with the size the SITE measured (not the form's).
|
|
388
|
+
const sent = await call(acme, 'POST', '/send', {type: 'bug', title: 'With a picture', description: 'See the shot.', attachments: [{...a, size: 1, type: 'text/plain'}]});
|
|
389
|
+
assert.equal(sent.status, 201, JSON.stringify(sent.body));
|
|
390
|
+
const report = (await call(manager, 'GET', `${P}/reports`)).body.reports.find(r => r.title === 'With a picture');
|
|
391
|
+
assert.deepEqual(report.attachments.map(x => [x.name, x.type, x.size, x.stored]), [['shot.png', 'image/png', png.length, true]]);
|
|
392
|
+
|
|
393
|
+
const file = await manager.inject({method: 'GET', url: `${P}/reports/${report.id}/attachments/${a.id}`});
|
|
394
|
+
assert.equal(file.statusCode, 200);
|
|
395
|
+
assert.equal(file.headers['content-type'], 'image/png');
|
|
396
|
+
assert.equal(file.headers['x-content-type-options'], 'nosniff');
|
|
397
|
+
assert.ok(Buffer.compare(file.rawPayload, png) === 0, 'the same bytes');
|
|
398
|
+
// The site sees the file in its list, and opens its own copy.
|
|
399
|
+
const mine = (await call(acme, 'GET', '/sent')).body.reports.find(r => r.title === 'With a picture');
|
|
400
|
+
assert.equal(mine.attachments[0].stored, true);
|
|
401
|
+
assert.equal((await acme.inject({method: 'GET', url: `/attachments/${a.id}`})).statusCode, 200);
|
|
402
|
+
|
|
403
|
+
// The fleet route: another site's token, an unannounced file and a wrong size are all refused.
|
|
404
|
+
const raw = (tok, att, body) => manager.inject({method: 'POST', url: `${P}/fleet/attach/${report.id}/${att}`,
|
|
405
|
+
headers: {authorization: `Bearer ${tok}`, 'content-type': 'application/octet-stream'}, payload: body});
|
|
406
|
+
assert.equal((await raw('tok-beta', a.id, png)).statusCode, 404, 'not beta\'s report');
|
|
407
|
+
assert.equal((await raw('tok-acme', '0f8fad5b-d9cb-469f-a165-000000000000', png)).statusCode, 404);
|
|
408
|
+
assert.equal((await raw('tok-acme', a.id, png)).statusCode, 200, 'already here: a retry is fine');
|
|
409
|
+
|
|
410
|
+
// Delete the report: its files go too.
|
|
411
|
+
assert.equal((await call(manager, 'DELETE', `${P}/reports/${report.id}`)).status, 200);
|
|
412
|
+
assert.equal((await manager.inject({method: 'GET', url: `${P}/reports/${report.id}/attachments/${a.id}`})).statusCode, 404);
|
|
413
|
+
await acme.close();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('a file attached while the manager restarts waits with its report, then both go', async () => {
|
|
417
|
+
const P = '/api/plugins/feedback';
|
|
418
|
+
const acme = await siteWithUploads('tok-acme', {id: 'u1', name: 'Sam'});
|
|
419
|
+
const up = (await acme.inject({method: 'POST', url: '/attachments', ...form('log.txt', 'text/plain', 'line one\nline two')})).json();
|
|
420
|
+
managerUp = false;
|
|
421
|
+
try {
|
|
422
|
+
const r = await call(acme, 'POST', '/send', {type: 'bug', title: 'Offline with a log', description: 'd', attachments: [up]});
|
|
423
|
+
assert.equal(r.body.state, 'waiting');
|
|
424
|
+
const local = (await call(acme, 'GET', '/sent')).body.reports.find(x => x.title === 'Offline with a log');
|
|
425
|
+
assert.equal(local.attachments[0].name, 'log.txt');
|
|
426
|
+
} finally { managerUp = true; }
|
|
427
|
+
await call(acme, 'GET', '/sent');
|
|
428
|
+
const report = (await call(manager, 'GET', `${P}/reports`)).body.reports.find(r => r.title === 'Offline with a log');
|
|
429
|
+
assert.equal(report.attachments[0].stored, true);
|
|
430
|
+
const file = await manager.inject({method: 'GET', url: `${P}/reports/${report.id}/attachments/${up.id}`});
|
|
431
|
+
assert.equal(file.body, 'line one\nline two');
|
|
432
|
+
await acme.close();
|
|
433
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings cog (1.1.7): each side keeps its own keys, values are whole
|
|
3
|
+
* numbers in range, and the server reads them when used - a changed
|
|
4
|
+
* per-site limit applies to the next report without a restart.
|
|
5
|
+
*/
|
|
6
|
+
import {test, after} from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import Fastify from 'fastify';
|
|
12
|
+
import feedback from '../plugin.js';
|
|
13
|
+
import {cleanSettings} from '../lib/shape.js';
|
|
14
|
+
|
|
15
|
+
const dirs = [];
|
|
16
|
+
after(async () => { for (const d of dirs) await fs.rm(d, {recursive: true, force: true}); });
|
|
17
|
+
const tmp = async () => { const d = await fs.mkdtemp(path.join(os.tmpdir(), 'feedback-set-')); dirs.push(d); return d; };
|
|
18
|
+
|
|
19
|
+
test('cleanSettings keeps one side\'s keys, in range, else current, else default', () => {
|
|
20
|
+
assert.deepEqual(cleanSettings({syncMinutes: '30', perSiteMax: 99}, {}, 'sender'), {syncMinutes: 30});
|
|
21
|
+
assert.deepEqual(cleanSettings({syncMinutes: 2}, {syncMinutes: 90}, 'sender'), {syncMinutes: 90}, 'below 5 keeps what is there');
|
|
22
|
+
assert.deepEqual(cleanSettings({perSiteMax: 0, perSiteMinutes: 'x'}, {}, 'receiver'), {perSiteMax: 10, perSiteMinutes: 10});
|
|
23
|
+
assert.deepEqual(cleanSettings({perSiteMax: 3, perSiteMinutes: 1.5, syncMinutes: 9}, {perSiteMinutes: 20}, 'receiver'), {perSiteMax: 3, perSiteMinutes: 20});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
async function receiver(store) {
|
|
27
|
+
const app = Fastify();
|
|
28
|
+
await app.register(feedback, {
|
|
29
|
+
auth: {authenticate: async (req) => { req.user = {id: 'v1', role: 'x'}; }, requirePermission: () => async () => {}},
|
|
30
|
+
prefix: '/api/plugins/feedback', dataDir: await tmp(), mode: 'receiver',
|
|
31
|
+
readSettings: async () => ({...store}), writeSettings: async (next) => { Object.assign(store, next); },
|
|
32
|
+
slugFromToken: async () => 'acme', siteNames: () => ({acme: 'Acme'}), hooks: {}
|
|
33
|
+
});
|
|
34
|
+
return app;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('the manager\'s GET/PUT /settings show and save its own two keys', async () => {
|
|
38
|
+
const store = {syncMinutes: 15, perSiteMax: 10, perSiteMinutes: 10};
|
|
39
|
+
const app = await receiver(store);
|
|
40
|
+
const got = (await app.inject({url: '/api/plugins/feedback/settings'})).json();
|
|
41
|
+
assert.deepEqual(got, {side: 'receiver', settings: {perSiteMax: 10, perSiteMinutes: 10}});
|
|
42
|
+
const put = await app.inject({method: 'PUT', url: '/api/plugins/feedback/settings', payload: {perSiteMax: 2, syncMinutes: 999}});
|
|
43
|
+
assert.equal(put.statusCode, 200);
|
|
44
|
+
assert.equal(store.perSiteMax, 2);
|
|
45
|
+
assert.equal(store.syncMinutes, 15, 'the other side\'s key is not touched');
|
|
46
|
+
await app.close();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a site\'s settings route is for administrators', async () => {
|
|
50
|
+
const app = Fastify();
|
|
51
|
+
await app.register(feedback, {
|
|
52
|
+
auth: {authenticate: async (req) => { req.user = {id: 'u', role: 'user'}; }, requirePermission: () => async () => {},
|
|
53
|
+
requireAdmin: async (req, reply) => reply.code(403).send({error: 'Forbidden'})},
|
|
54
|
+
mode: 'sender', timer: false, env: {}, dataDir: await tmp(), readSettings: async () => ({syncMinutes: 60}), hooks: {}
|
|
55
|
+
});
|
|
56
|
+
assert.equal((await app.inject({url: '/settings'})).statusCode, 403);
|
|
57
|
+
assert.equal((await app.inject({method: 'PUT', url: '/settings', payload: {syncMinutes: 5}})).statusCode, 403);
|
|
58
|
+
await app.close();
|
|
59
|
+
});
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import {
|
|
4
4
|
buildPayload, cleanSubmission, cleanUpdate, createThrottle, issueDescription, receivedReport, senderView, statusFromIssue
|
|
5
5
|
} from '../lib/shape.js';
|
|
6
|
-
import {mergeReports} from '../lib/sender.js';
|
|
6
|
+
import {createOutbox as createFbOutbox, droppedByManager, mergeReports} from '../lib/sender.js';
|
|
7
7
|
|
|
8
8
|
test('a submission is cleaned, and says nothing about who sent it', () => {
|
|
9
9
|
assert.equal(cleanSubmission({title: ' ', description: 'x'}).error, 'Give it a short title.');
|
|
@@ -85,3 +85,89 @@ test('the list merges the manager view with what is still waiting', () => {
|
|
|
85
85
|
assert.equal(mergeReports(outbox, remote, {e1: '2026-09-27T12:00:00Z'})[1].unread, false, 'read once seen');
|
|
86
86
|
assert.equal(mergeReports(outbox, null, {}).length, 2, 'manager unreachable: the outbox copy stands in');
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
// ---- Attachments (1.1.6) ----
|
|
90
|
+
|
|
91
|
+
import {ATTACHMENTS, cleanAttachment, cleanAttachments, safeFileName} from '../lib/shape.js';
|
|
92
|
+
import {createStore} from '../lib/attachments.js';
|
|
93
|
+
import fs from 'node:fs';
|
|
94
|
+
import os from 'node:os';
|
|
95
|
+
import path from 'node:path';
|
|
96
|
+
|
|
97
|
+
const ID = '0f8fad5b-d9cb-469f-a165-70867728950e';
|
|
98
|
+
|
|
99
|
+
test('an attachment: a known type, a server-made id, a size within the cap, a safe name', () => {
|
|
100
|
+
assert.deepEqual(cleanAttachment({id: ID, name: 'shot.png', type: 'image/png', size: 10, extra: 'x'}), {id: ID, name: 'shot.png', type: 'image/png', size: 10});
|
|
101
|
+
assert.equal(cleanAttachment({id: ID, name: 'x.svg', type: 'image/svg+xml', size: 10}), null, 'SVG can carry script');
|
|
102
|
+
assert.equal(cleanAttachment({id: ID, name: 'x.html', type: 'text/html', size: 10}), null);
|
|
103
|
+
assert.equal(cleanAttachment({id: '../../etc/passwd', name: 'x.png', type: 'image/png', size: 10}), null, 'the id becomes a file name');
|
|
104
|
+
assert.equal(cleanAttachment({id: ID, name: 'x.png', type: 'image/png', size: ATTACHMENTS.maxBytes + 1}), null);
|
|
105
|
+
assert.equal(cleanAttachment({id: ID, name: 'x.png', type: 'image/png', size: 0}), null);
|
|
106
|
+
assert.equal(cleanAttachment({id: ID, name: 'x.png', type: 'IMAGE/PNG; q=1', size: 3}).type, 'image/png');
|
|
107
|
+
assert.equal(cleanAttachment({id: ID, name: '..\\\\..\\\\evil".png', type: 'image/png', size: 3}).name, 'evil_.png', 'last segment, no quotes');
|
|
108
|
+
assert.equal(safeFileName('', 'pdf'), 'file.pdf');
|
|
109
|
+
assert.equal(safeFileName('...hidden'), 'hidden');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('a report carries at most three files, each once, and the manager marks them not yet stored', () => {
|
|
113
|
+
const many = [1, 2, 3, 4].map(n => ({id: `${ID.slice(0, -1)}${n}`, name: `f${n}.png`, type: 'image/png', size: 5}));
|
|
114
|
+
assert.equal(cleanAttachments([...many, many[0]]).length, 3);
|
|
115
|
+
assert.equal(cleanAttachments([many[0], many[0]]).length, 1);
|
|
116
|
+
assert.deepEqual(cleanAttachments('nope'), []);
|
|
117
|
+
const {report} = cleanSubmission({title: 't', description: 'd', attachments: [many[0], {id: 'bad', type: 'image/png', size: 1}]});
|
|
118
|
+
assert.equal(report.attachments.length, 1);
|
|
119
|
+
const {data} = receivedReport({title: 't', description: 'd', attachments: [many[0]]}, 'acme');
|
|
120
|
+
assert.equal(data.attachments[0].stored, false);
|
|
121
|
+
const view = senderView({id: 'e1', data: {...data, attachments: [{...data.attachments[0], stored: true, secret: 'x'}]}});
|
|
122
|
+
assert.deepEqual(view.attachments, [{id: many[0].id, name: 'f1.png', type: 'image/png', size: 5, stored: true}]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('control characters are stripped, so nothing is too big to ever send', () => {
|
|
126
|
+
const {report} = cleanSubmission({title: 't\u0001', description: `${'\u0001'.repeat(8000)}real`});
|
|
127
|
+
assert.equal(report.title, 't');
|
|
128
|
+
assert.equal(report.description, 'real');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('the file store: private files, read back by id, unclaimed ones tidied', () => {
|
|
132
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fb-files-'));
|
|
133
|
+
try {
|
|
134
|
+
const files = createStore(path.join(root, 'attachments'));
|
|
135
|
+
const a = files.save({id: ID, name: 'a.txt', type: 'text/plain', size: 2}, Buffer.from('hi'));
|
|
136
|
+
assert.equal(files.read(a).toString(), 'hi');
|
|
137
|
+
assert.deepEqual(files.meta(ID), a);
|
|
138
|
+
assert.equal(fs.statSync(path.join(root, 'attachments', `${ID}.txt`)).mode & 0o777, 0o600);
|
|
139
|
+
assert.equal(fs.statSync(path.join(root, 'attachments')).mode & 0o777, 0o700);
|
|
140
|
+
assert.equal(files.meta('../x'), null);
|
|
141
|
+
assert.throws(() => files.save(a, Buffer.from('x'), '../up'), /folder/);
|
|
142
|
+
// A day old and named by no report: gone. Named by one: kept.
|
|
143
|
+
const old = new Date(Date.now() - 2 * 86400_000);
|
|
144
|
+
fs.utimesSync(path.join(root, 'attachments', `${ID}.json`), old, old);
|
|
145
|
+
fs.writeFileSync(path.join(root, 'attachments', `${ID}.json`), JSON.stringify({...a, at: old.toISOString()}));
|
|
146
|
+
fs.utimesSync(path.join(root, 'attachments', `${ID}.json`), old, old);
|
|
147
|
+
assert.equal(files.prune({keep: new Set([ID])}), 0);
|
|
148
|
+
assert.equal(files.prune(), 1);
|
|
149
|
+
assert.equal(files.read(a), null);
|
|
150
|
+
} finally { fs.rmSync(root, {recursive: true, force: true}); }
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('a report deleted on the manager is forgotten by the site (its text, then its files)', async () => {
|
|
154
|
+
const {mkdtempSync, rmSync} = await import('node:fs');
|
|
155
|
+
const {tmpdir} = await import('node:os');
|
|
156
|
+
const {join} = await import('node:path');
|
|
157
|
+
assert.equal(droppedByManager({ref: 'a', state: 'sent'}, [{ref: 'b'}]), true);
|
|
158
|
+
assert.equal(droppedByManager({ref: 'a', state: 'sent'}, [{ref: 'a'}]), false);
|
|
159
|
+
assert.equal(droppedByManager({ref: 'a', state: 'waiting'}, []), false, 'never sent: the manager cannot have it yet');
|
|
160
|
+
assert.equal(droppedByManager({ref: 'a', state: 'sent'}, null), false, 'manager unreachable: keep everything');
|
|
161
|
+
const dir = mkdtempSync(join(tmpdir(), 'fb-outbox-'));
|
|
162
|
+
try {
|
|
163
|
+
const box = createFbOutbox(dir);
|
|
164
|
+
for (const ref of ['kept', 'gone', 'wait']) box.add({ref, attachments: [{id: `${ref}-file`}]});
|
|
165
|
+
box.mark('kept', {state: 'sent'});
|
|
166
|
+
box.mark('gone', {state: 'sent'});
|
|
167
|
+
assert.equal(box.forgetDropped(null), 0);
|
|
168
|
+
assert.equal(box.forgetDropped([{ref: 'kept'}]), 1);
|
|
169
|
+
assert.deepEqual(box.all().map(i => i.ref), ['kept', 'wait']);
|
|
170
|
+
} finally {
|
|
171
|
+
rmSync(dir, {recursive: true, force: true});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
@@ -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@0525295",
|
|
4
4
|
"plugins": {
|
|
5
5
|
"blog": {
|
|
6
|
-
"version": "1.9.
|
|
6
|
+
"version": "1.9.7",
|
|
7
7
|
"files": {
|
|
8
|
-
"CLAUDE.md": "
|
|
8
|
+
"CLAUDE.md": "641e27358f95bf4415e9cfc507645d45f4c6a15f1ca9ebef753068f43ec867ff",
|
|
9
9
|
"admin/css/index.css": "6142a6b756e1218157842528830b1e84f592f0b84b7d5ce6449db421b6f4b039",
|
|
10
10
|
"admin/templates/blog.html": "a6054c1febe764bb1edf5c9e466d44a9080d9c83ba5486437b7a8244acf5ba54",
|
|
11
11
|
"admin/templates/categories.html": "ab7d6fc2fedd48876c585bc216a1532b3bdd78c946dee976c1171bfe4664feb6",
|
|
@@ -43,15 +43,16 @@
|
|
|
43
43
|
"config.js": "ee67396262762f5c31269d258affd75f7392a3e364f72f7792c2f1cdafd94bba",
|
|
44
44
|
"docs/guide.md": "d9f70afe4612a2464fc537a0348e987873d09cc30ce44692a22cf79c40a4c0b4",
|
|
45
45
|
"lib/access.js": "af47440c7233a56000641a3231d7e9ff81b12f40dfb9528aa6dd4e2dd9bca8df",
|
|
46
|
+
"lib/cache.js": "5623745e580e0d5ea4ca48ee528427a6efabafec777f8607154dcb662f9147eb",
|
|
46
47
|
"lib/layouts.js": "dbb6ae9e514ccaf600a732702925b905131dea166d90fe759a62b6652e78e1de",
|
|
47
48
|
"lib/nav-link.js": "6a8f9368bb8aee1c21e90fb335a691859cb9a9bc250c2b9167a33ca9189fa2ff",
|
|
48
49
|
"lib/notices.js": "a9a2918ee6f5f7e0bbaeced0d3539584279bdbe68d15472a8615bdddb4ef905b",
|
|
49
|
-
"lib/page.js": "
|
|
50
|
+
"lib/page.js": "02ef5b0e5aadfe5a8a5de4b7471518b24193b1dad2bda23bea06c6cc1e4743e7",
|
|
50
51
|
"lib/render.js": "0e97c6a950bc962c8d8ea4b2f21d06cb73fbcdc738d8942d6935d01309a29b1d",
|
|
51
52
|
"lib/samples.js": "e8cdb77ce22e43937707f6854d3a5cb722437dc584e22ac7b24480bef686cf85",
|
|
52
|
-
"plugin.js": "
|
|
53
|
-
"plugin.json": "
|
|
54
|
-
"plugin.public.js": "
|
|
53
|
+
"plugin.js": "4a2b1cd87a7d91889d3a7d87bc1ada2909a5127e95fc2e5448fd814b16ee83bb",
|
|
54
|
+
"plugin.json": "b5c2ab90d78c3cb8c4cfe7b72f66e3c5237fbbb48bb640b4490b3afbd1d45b10",
|
|
55
|
+
"plugin.public.js": "2aac0a56584e7aec30000f7718256d7ae4cf889b821f44d4848b435ed031bd39",
|
|
55
56
|
"public/blog.css": "fd733e6b624190da1e70dc1b6613e2376efd7f7570a28fab311d81006d03d960",
|
|
56
57
|
"public/blog.js": "d01c72a9834dcc7c5241923371f2b164ac30d20ac9f1a5c5dc5b71fa3cb09f08",
|
|
57
58
|
"public/samples/aurora.svg": "feb29e2b46465913fc29de9eda221b00b1541f1fa3ea7120f0c1d1cd11a9a4f6",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"templates/index.html": "3c2cf8a4ffde8599d451f6d00b736aeba42fd073b726ada3b0ffe4d56e1ef0f4",
|
|
66
67
|
"templates/post.html": "28e9f06071115ecc4fbba007b35782732bf12ef72528525893d424e3c4f0313f",
|
|
67
68
|
"tests/access.test.js": "65b760910117c3a44195543acdcff91074f39b4e5567911a2bd8676204152cb2",
|
|
69
|
+
"tests/cache.test.js": "f823d9ff65c5b5594a8e3d1d3b598cc3524f10885ddf34017cf0a9364878a98f",
|
|
68
70
|
"tests/comment-notices.test.js": "34026cc9a2a4746cf87bc41df8be849fadaa43091a72ac2aa1d06c1f437bf618",
|
|
69
71
|
"tests/layouts.test.js": "554cff8194bd650cfb5f2a83c1f51c25f427c07f7dbfdc2ad9ad69bb38fca6a0",
|
|
70
72
|
"tests/nav-link.test.js": "d36e33ad7483abca27ae6e2098ad2f75bafa18d91f3beba70372ebe6b9b88994",
|
|
@@ -73,22 +75,22 @@
|
|
|
73
75
|
}
|
|
74
76
|
},
|
|
75
77
|
"booking": {
|
|
76
|
-
"version": "1.6.
|
|
78
|
+
"version": "1.6.4",
|
|
77
79
|
"files": {
|
|
78
|
-
"CLAUDE.md": "
|
|
80
|
+
"CLAUDE.md": "76debc74ef2fcf4e9a78bb9256ade4ffbc179541876f6e58d00a8cc0e0e216e1",
|
|
79
81
|
"admin/css/index.css": "4f7731921c0c5038cb1566a6ab2d43ffc35bf9b161e58f87a0cff423e81b47fa",
|
|
80
82
|
"admin/lib/details.js": "8b37536bfa9ccfcc5265cc957ba8266d3f5f777133e7c0946ae49540f1c80e3c",
|
|
81
|
-
"admin/lib/diary-layout.js": "
|
|
83
|
+
"admin/lib/diary-layout.js": "d0e36674ff454e5217b02c3ef3b820e4dece97a62df618a6052016dfeefab1c9",
|
|
82
84
|
"admin/lib/extension-sections.js": "0588ad47660e511b0dd8077bb9c3999c775d97597333a75e87ee12d915bbf00b",
|
|
83
85
|
"admin/lib/kit.js": "b69b02172d76ef80879da3f003cc1e687c6149626c9e58af51729c2e4a3ff959",
|
|
84
86
|
"admin/templates/booking.html": "20b9a3bfe3e5d5bbee613f29be99551b12790fb38f80d119585360cc38e9ec57",
|
|
85
87
|
"admin/views/booking.js": "4eddf6c0b60dbbba4c6e35352df247a3d34b66ee1d43c51fe2d53f894707348f",
|
|
86
88
|
"admin/views/bookings.js": "bf17db82ed4d2f5c29ac924dd60dc0b84c8bd9c8ce32da6cbb011527699a56a6",
|
|
87
|
-
"admin/views/diary.js": "
|
|
89
|
+
"admin/views/diary.js": "6cdbf81106a5b8ec2ed905f2e30df8ee626db2db51241bc9b41499ea2ed6ad27",
|
|
88
90
|
"admin/views/panels.js": "1d269d90553a5482fd6ebd18180cb736011770f0204d5c1306948abcb4b676b2",
|
|
89
|
-
"admin/views/people.js": "
|
|
91
|
+
"admin/views/people.js": "11e9bb811378a288bb44e879d84251b8e9aaeee68490d64f4db44d8727c38376",
|
|
90
92
|
"admin/views/services.js": "122326d222ed16dc8ec903fef6f62f6a7638c31d73c4612b7a8c2ced38358068",
|
|
91
|
-
"admin/views/settings.js": "
|
|
93
|
+
"admin/views/settings.js": "958a4f99df843fbcb884adeab4f768ebb2338fff5c361e8bbad3ddc90ef992e0",
|
|
92
94
|
"collections/booking-bookables.json": "9f1f305eceac3d880413d5477a631e48c1fed7ff389c434f34651f4082ca1a1c",
|
|
93
95
|
"collections/booking-bookings.json": "fa0b15cc81b4a2114734a0bc8adc4b4d25049d16cf62549b83634cdbeb543b52",
|
|
94
96
|
"collections/booking-services.json": "cecb500a9215a0eba894e6b4c9ae78f5fbeb209830d07d47050c56c75143f66a",
|
|
@@ -100,7 +102,7 @@
|
|
|
100
102
|
"lib/mail.js": "cad8f409df62d6e42f0f4024ec364bbe018a62b842fa3f7c38786fd4e7f92408",
|
|
101
103
|
"lib/store.js": "7aa7967e4694e5a3b85378f60302e5a47394b1d90c7dd86e09578156f1c13e03",
|
|
102
104
|
"plugin.js": "4a303855a5a7f746e07e45a937dfeac8984dc9254c2f9b1a60b6f6306912ed84",
|
|
103
|
-
"plugin.json": "
|
|
105
|
+
"plugin.json": "8d90802c7c8c3681903b69200b19af88d326242bc33742bb8c61777c8b99775d",
|
|
104
106
|
"plugin.public.js": "4d5b4e8ad6943182ee3b6c6e6f6762a2d3f676d8d7ee363fcb07bd47d3d6532e",
|
|
105
107
|
"public/booking.css": "8b88989fdd04e50efc1688a2f32fe2e422299d7d13b81bb8fce0cc162fa99df6",
|
|
106
108
|
"public/booking.js": "7dfc3d205696967013886567f67a867443c2e370bac29019d69911d695fdbade",
|
|
@@ -109,43 +111,47 @@
|
|
|
109
111
|
"public/lib/slots.js": "6472e530f3f521e7a08625ed2ee86802a9065cbf2f97c2c7601229f5f032dc07",
|
|
110
112
|
"public/lib/time.js": "48e13a23e3d7a4916e2097e8f87c03a87d37b71c4ef037a5a16143c6a4185c41",
|
|
111
113
|
"templates/booking.html": "a4995de46dfdab7e692b56f21347f29c76320cf3b0477889dc3c5d14e0b6dcd7",
|
|
112
|
-
"tests/admin-diary.test.js": "
|
|
114
|
+
"tests/admin-diary.test.js": "6bd117af798e748234c94e4e66339ff1ec3bba9633790a2a5fd291f64c30e484",
|
|
113
115
|
"tests/api.test.js": "efb3767f7efde08c11c8edcb5753889773b421188798f725aff0e4e533860aff",
|
|
114
116
|
"tests/public.test.js": "0b7b626d60df5a3d73d7c0077b73f59b62a9fded98ce84a8d93cd32cd0333159",
|
|
115
117
|
"tests/pure.test.js": "8ba0c8f76b4c9efcc422f39ff29509612544d81f760025a4b9a5594af14e7bcf"
|
|
116
118
|
}
|
|
117
119
|
},
|
|
118
120
|
"feedback": {
|
|
119
|
-
"version": "1.1.
|
|
121
|
+
"version": "1.1.8",
|
|
120
122
|
"files": {
|
|
121
|
-
"CLAUDE.md": "
|
|
122
|
-
"admin/css/index.css": "
|
|
123
|
-
"admin/lib/kit.js": "
|
|
123
|
+
"CLAUDE.md": "8666d4ef8d8620f0fc20f544a4c1915cddc21370cbc27a7f854ebfe41f4cfe1d",
|
|
124
|
+
"admin/css/index.css": "c8f827ad62386134e87bc05bd2c8e24f41184402b0a5a17e7250d749240c34c0",
|
|
125
|
+
"admin/lib/kit.js": "502d2d090e8aaeda03034864b4e3cc0d9bbf7e8fba29b6e4916a8a758c0cd54d",
|
|
126
|
+
"admin/lib/settings.js": "6e3fe9103fcbf7acb52211698ca408fac067e061100e580a9a3011cd76bdfba3",
|
|
124
127
|
"admin/templates/feedback.html": "a366dce78f6c3e84a7cb0ac63eba53e1980a4eb44154496a0536f5214c5bb36f",
|
|
125
|
-
"admin/views/feedback.js": "
|
|
126
|
-
"admin/views/inbox.js": "
|
|
127
|
-
"admin/views/send.js": "
|
|
128
|
+
"admin/views/feedback.js": "5aed85f09ef8af762d425912ca7fb4299ec129a21822863edf431af4f1a12e64",
|
|
129
|
+
"admin/views/inbox.js": "20cc1bd16df59c775f0e2d398f4179d0e30683a3a76a942e250f59f7cbdce98a",
|
|
130
|
+
"admin/views/send.js": "104290b8f58f7e8d75d86de0b3f5496bd38546b6bfb20ddff45e153e61f7d061",
|
|
128
131
|
"collections/feedback-reports.json": "a072291b52463f7785a77f5f77d36b8238ba8f89cfc2672c6642eccdb3144486",
|
|
129
132
|
"config.js": "29d9999d1028c2c11931f67fb9abdf35132430b7cf58c161681e734c0b45143a",
|
|
130
|
-
"docs/guide.md": "
|
|
131
|
-
"lib/
|
|
132
|
-
"lib/
|
|
133
|
-
"lib/
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
133
|
+
"docs/guide.md": "a2a14f834da9fcb59222249d925ad3a387a1c7e7ec03aad84e0f512f936cc670",
|
|
134
|
+
"lib/attachments.js": "b23bda973c2e7723bc485c5f2840868363a3ac4e65c153cb5abd5a3c41dba3ad",
|
|
135
|
+
"lib/cache.js": "6db2859c21a3dddcc2ec34e8380dd672fc99a6cb0d76a2b05b68086503f4ccce",
|
|
136
|
+
"lib/receiver.js": "98aeea6712ba6aa576919520a86451f7f223e5154416a225a848e0c875923c8d",
|
|
137
|
+
"lib/sender.js": "58194d1ab562e55e7ac6a29aed6b5ec8316ec6932b40b44034bb347e8f799f79",
|
|
138
|
+
"lib/shape.js": "47ccbbb95598023f8de03dd0f557205159375fd2bb61ee799a19c186c45bc4d9",
|
|
139
|
+
"plugin.js": "dd9cf5066e8491184a782112ed9463a211b37c2dbd93caf832edd65c2bb0e2a4",
|
|
140
|
+
"plugin.json": "9712d3eba3d81a0a8f5f85dfa168ea158d14b6ef9c0de6416314938c1b41f6ed",
|
|
141
|
+
"tests/api.test.js": "67336ed4a034e21ead5876becefa1de2fa2229b06689385471516df92e198d33",
|
|
142
|
+
"tests/settings.test.js": "3a50ac89a9d114c8466fc043a66f0f1bf373e7a7ad6dba45a995468c3332b374",
|
|
143
|
+
"tests/shape.test.js": "2e2e5e17ce059fbaa4dad8170ae45382d6c7e74bacf3b301814bb81a60ba98ba"
|
|
138
144
|
}
|
|
139
145
|
},
|
|
140
146
|
"mail-reader": {
|
|
141
|
-
"version": "1.2.
|
|
147
|
+
"version": "1.2.8",
|
|
142
148
|
"files": {
|
|
143
|
-
"CLAUDE.md": "
|
|
149
|
+
"CLAUDE.md": "32132446515e66f6784d01b11b1ded8913b0778087472edb667bfd83cc3a871c",
|
|
144
150
|
"admin/views/mail.js": "c5c6483f9618c75cac8f4be32d3aea99511db76d4b9d38b8ef43bdc1d35db3c5",
|
|
145
151
|
"config.js": "6e65803ad822a9acda40c203631b8eb32a87cf9d9e5f1c7cb59d01aa406075c6",
|
|
146
152
|
"docs/guide.md": "d6b5944740ea1bbd689034ef726d8bc0f9bfeb28ea62b63c258292962d45cf18",
|
|
147
153
|
"plugin.js": "7f602a60c4eca7d3637659d03d5de9e242584666846b16c9e5a1b758e4856c9f",
|
|
148
|
-
"plugin.json": "
|
|
154
|
+
"plugin.json": "e5ac7f3496d9a535fa4899d1de87a514535084d13acecd6a5897744b6174807a"
|
|
149
155
|
}
|
|
150
156
|
},
|
|
151
157
|
"security": {
|
|
@@ -175,36 +181,37 @@
|
|
|
175
181
|
}
|
|
176
182
|
},
|
|
177
183
|
"shopping-cart": {
|
|
178
|
-
"version": "1.6.
|
|
184
|
+
"version": "1.6.3",
|
|
179
185
|
"files": {
|
|
180
|
-
"CLAUDE.md": "
|
|
181
|
-
"admin/css/index.css": "
|
|
186
|
+
"CLAUDE.md": "1c881307d422beaeaef405b9d90ec4c71679fdebe0a919353fbd9c597e26b22b",
|
|
187
|
+
"admin/css/index.css": "dcb2d30126c2e049b03a2e2b615082a067ab4dc911c87bf128029c900e6691b4",
|
|
182
188
|
"admin/lib/chart.js": "b01ce5cfac773c8df8efccab50b67d3dfa99fbead0037d817553db4a6688d4d6",
|
|
183
189
|
"admin/lib/kit.js": "d9aa90469b3f589beaff0c3fb6ad3d983ab3fd6d94c06965be639a665bfe052f",
|
|
184
190
|
"admin/templates/shop.html": "c5bb03f1f7cbabfbd8202f3533783eb1848103755b1ba25fdcf1fd8e9478cb8e",
|
|
185
|
-
"admin/views/orders.js": "
|
|
191
|
+
"admin/views/orders.js": "1c5b571027c78223f98dec2a39c1af8e33e0d810a9b34e0b176ebf85c5c9f486",
|
|
186
192
|
"admin/views/overview.js": "97045c069c6560b3f67e1b2230bdc9c4133ed92ec6f732886470f886f11c755a",
|
|
187
193
|
"admin/views/products.js": "e109660355aaec8db07eb65fa1169e7d2ec20bc2ec45293cc20e34f7c1bd62bf",
|
|
188
194
|
"admin/views/sales.js": "da71b9fbb7d39e39ecd8be35b1187e88bc4b5b28f18baaeddc6ea4cd202bb330",
|
|
189
195
|
"admin/views/settings.js": "54d4b52409d0bdd1eb4c6e4d86c07a285914659eb6b7053183f77e2fb4b9feaa",
|
|
190
|
-
"admin/views/shop.js": "
|
|
196
|
+
"admin/views/shop.js": "bfc2603095cda6fd8ce3b3c10428ed55d748622d020159b07d178e6f76e3e517",
|
|
191
197
|
"collections/shop-orders.json": "0bca8eef2d5f9108651f73d273480265c167dc983aa115ce59613d13015ca255",
|
|
192
198
|
"collections/shop-products.json": "4d4613ecec7b8a5a0744c4f0a57b17a4429b461e060206a4bbdea5ad7c8c75dc",
|
|
193
199
|
"config.js": "31b336560d21a802d799ed487d5c3116de898446ba3521068b10d192f4ebbb2d",
|
|
194
|
-
"docs/guide.md": "
|
|
200
|
+
"docs/guide.md": "10fc12a0af35d648f79762092643bcf38ce7ad74ff5a50ecb3b833ecd109be70",
|
|
195
201
|
"inject/body.html": "7e7e5b408f47abb8650e0a7d5e277daf8a634f4c6877c5259f837b1bf3d6c8e8",
|
|
196
202
|
"lib/accounts.js": "5115cec6a145da18c106dfdbf5fd0b2a1bad0c68e8fac4ef623287e85db75e5c",
|
|
197
203
|
"lib/commission.js": "5b10475d98cff2aaf4c76b9950f8d698d6262e36754a8963669690a8e3e9bd52",
|
|
198
204
|
"lib/extensions.js": "73d3515b52c066e3e616c425d91a7a48a6aab153e7d14ae7278b38ee32649352",
|
|
199
|
-
"lib/payments.js": "
|
|
205
|
+
"lib/payments.js": "d2a39bf42373c78c716c1a35f606f9cce1901d0dab5f6bd00bebde9b35f0328c",
|
|
200
206
|
"lib/pricing.js": "5644f2dd55fc3cfd7f40b0169dea5fb547e8b302239b8c17f23cfa177e42eab5",
|
|
207
|
+
"lib/refunds.js": "73db567d97d40f68806c0d8dcb7235a12f5f86662739febf4b2229695ace40e4",
|
|
201
208
|
"lib/render.js": "2a2b9ab4a24728186e1e45e6429c7406a6d5ca3d4b43db1758bbd54405d105b7",
|
|
202
|
-
"lib/sales.js": "
|
|
209
|
+
"lib/sales.js": "bc6c39d70c2ce83f4109474071e6b048dc6554c4b11ac03dbeccbdfdb5881368",
|
|
203
210
|
"lib/samples.js": "912b3d5bd543dffa5f870caf3c2b873f642fd55e3ebf3f97e1b31eeaaac77d3b",
|
|
204
|
-
"lib/shop.js": "
|
|
211
|
+
"lib/shop.js": "baa5dfc4e410210e8ba13678997ee5752eb2ebe72fc4d3e449465d29b60b458d",
|
|
205
212
|
"lib/summary.js": "cdb13ce3d1799d429059fa03f179281711dc00427ccf2eb6b8f8aa52fef26295",
|
|
206
|
-
"plugin.js": "
|
|
207
|
-
"plugin.json": "
|
|
213
|
+
"plugin.js": "824dba8336846d443b2427ed9a5adeba11740570566fc6e89257f9847a488632",
|
|
214
|
+
"plugin.json": "475c64d3e5f07d0dd4f1480b65630d0e9ef362418653477de9be617aac13dc10",
|
|
208
215
|
"plugin.public.js": "2f92e8340c9da653401c11dd99dab033dc6772d63fa2114489192c48e9e0be00",
|
|
209
216
|
"public/account.js": "5f644016c098c7879c59948d662f5997cb61680996204ecb7897baab8945d434",
|
|
210
217
|
"public/cart.js": "7338141392ec653ff616eb75688d2a2ad08cff8427d7020d381f540be0037b73",
|
|
@@ -228,33 +235,38 @@
|
|
|
228
235
|
"roles/shop-customer.json": "3f3366c536c9fbf2b2ce146bb3090b008d5fbfe22752e764eb8e5a1de9cd611c",
|
|
229
236
|
"templates/shop.html": "4649e57d23458833a157799dc80141824eb0f658d96734bd1859c68e0ba9a8b9",
|
|
230
237
|
"tests/accounts.test.js": "e64adc2a855d4ca017682bba7f75b052534f826556d8d35d825fb79cbc66596a",
|
|
231
|
-
"tests/api.test.js": "
|
|
232
|
-
"tests/commission.test.js": "
|
|
233
|
-
"tests/pure.test.js": "
|
|
238
|
+
"tests/api.test.js": "15c71751a3d31171707cbb3119d21b85c1c59fb7a67272b318dd4655809ac8d2",
|
|
239
|
+
"tests/commission.test.js": "fbc4cf83e0ed6446a09e57e4ff37a9608c81947c633c78ac6049eb0aedf6cb58",
|
|
240
|
+
"tests/pure.test.js": "59a351c813784e8131b96a118c79de2e3858a2fba0fc5c0a6baed79e28cafda8",
|
|
241
|
+
"tests/refunds.test.js": "0ae06afca3cf9fd1fe89f1fbc63b3676216c79e174dbcb83788180c01d078366",
|
|
234
242
|
"tests/sales.test.js": "9025bc608b04bcc14f5bb2c98b4f7c35de99f3ddab4ffa59772d045795bf2cb7"
|
|
235
243
|
}
|
|
236
244
|
},
|
|
237
245
|
"support": {
|
|
238
|
-
"version": "1.1.
|
|
246
|
+
"version": "1.1.6",
|
|
239
247
|
"files": {
|
|
240
|
-
"CLAUDE.md": "
|
|
241
|
-
"admin/css/index.css": "
|
|
242
|
-
"admin/lib/kit.js": "
|
|
248
|
+
"CLAUDE.md": "65845bbf6a909cd523466896874e84a2a29d239fdc5da0840df5155913cdcf23",
|
|
249
|
+
"admin/css/index.css": "6695029b1da2a2e4ff25db4abecd360895ed39e4a9bc7f0996857672faf2c426",
|
|
250
|
+
"admin/lib/kit.js": "3d5a5fb5c53f7cc9ff98664803ff58701a4d8598068e9f1a16e9db0a72109c40",
|
|
251
|
+
"admin/lib/settings.js": "3f0b7c4c5ab927b2785af2abb3872ffd9269d12b402468f79795c4198eb49ea0",
|
|
243
252
|
"admin/templates/support.html": "2bb69d0443a367a402e6160fe68f4af3e8983b578cfff9575b4df9a534fcb480",
|
|
244
|
-
"admin/views/desk.js": "
|
|
245
|
-
"admin/views/inbox.js": "
|
|
246
|
-
"admin/views/support.js": "
|
|
253
|
+
"admin/views/desk.js": "4f3cffe3603592d715c6a9b553e02dc7331a2675cabaeddff709c91436531734",
|
|
254
|
+
"admin/views/inbox.js": "8134d91e20a19eb2ca7b25edb604fa241e65b764e0d058ee5d8751f903a16c32",
|
|
255
|
+
"admin/views/support.js": "d1c72cc2e35609e54be296791cdcceb43be9be016b6c307000ddb8a226ba6d92",
|
|
247
256
|
"collections/support-tickets.json": "55a0856bacd66c2e8f1e3c85bdf3638e5ff826560943cda854a8f2d336a88905",
|
|
248
257
|
"config.js": "eb7ccc1169fa8b2be52a213536825492fedaec5fa73fd749cba9f498dd54b503",
|
|
249
|
-
"docs/guide.md": "
|
|
258
|
+
"docs/guide.md": "bfe570f4f485f58ae3aab4f71039e5e426f85f6f348c96794dfcb2dc494fced1",
|
|
259
|
+
"lib/attachments.js": "e3479c7fe9f01e6287126279bc9d514fd14f00b64af9082ac49596bc6a543c23",
|
|
260
|
+
"lib/cache.js": "70cc826b24cc5359597a5c83a990f6da43cadebf2ac34094aee42058240fb72d",
|
|
250
261
|
"lib/mail.js": "d14aae3a59aad6af61451863cdaaf94fc4a87810b16e1dcb9c655dda6c803b3b",
|
|
251
|
-
"lib/receiver.js": "
|
|
252
|
-
"lib/sender.js": "
|
|
253
|
-
"lib/shape.js": "
|
|
254
|
-
"plugin.js": "
|
|
255
|
-
"plugin.json": "
|
|
256
|
-
"tests/api.test.js": "
|
|
257
|
-
"tests/
|
|
262
|
+
"lib/receiver.js": "c9401cc799e5eafd419c136c95826ca28a8455e32e99cea32f4821acc1fb6c26",
|
|
263
|
+
"lib/sender.js": "2b308422ac60a8a2801570e06319d5972797a601640c67beee95ccab9ac64092",
|
|
264
|
+
"lib/shape.js": "d60b48410aa5582ea80451c3d831686b0e29525a958733a8bc9dfaae8eca94d2",
|
|
265
|
+
"plugin.js": "91fff8d66bf5fa0fa4502bb177460ae234f0ed0494a4fbe6dd94d63e2f88b571",
|
|
266
|
+
"plugin.json": "70bf4f117c4b2f9132365f73a1b1a38c3dfd5618d5273a8c899a2393323ebf43",
|
|
267
|
+
"tests/api.test.js": "1af65f0cba8a83c080a6e3e65544abc86fc43c8e25ca05c7f3e1ab4e3f7ac5a2",
|
|
268
|
+
"tests/settings.test.js": "434acbfd940eef800b1166a9aaf36363351af5652fcfb06527b8498866b7b964",
|
|
269
|
+
"tests/shape.test.js": "7866c6c1d140716bf0817449937d6d711d7c3b97be1db1e1eb33bde341e36296"
|
|
258
270
|
}
|
|
259
271
|
}
|
|
260
272
|
}
|