94 lines
3.4 KiB
JavaScript
94 lines
3.4 KiB
JavaScript
|
|
/**
|
||
|
|
* Publish-mode policy — read + write + enumerate.
|
||
|
|
*
|
||
|
|
* Three modes, named after what they do rather than numbered:
|
||
|
|
* `append` Any valid listing goes in; readers see the signed scan report
|
||
|
|
* and their trust lists decide (the launch default).
|
||
|
|
* `scan-first` Infected uploads are rejected before they land. Clean ones
|
||
|
|
* publish immediately. Already the current /api/upload shape,
|
||
|
|
* so this mode is the default behaviour today.
|
||
|
|
* `review` Clean listings sit in a human-review queue before browse
|
||
|
|
* shows them. Not yet enforced — the toggle persists, the
|
||
|
|
* queue logic lands in a later milestone.
|
||
|
|
*
|
||
|
|
* Global mode is the fallback; a per-category override is read when a listing's
|
||
|
|
* category has one set. The admin UI exposes both.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { getSetting, setSetting } from './db.js';
|
||
|
|
import { logAudit } from './audit.js';
|
||
|
|
|
||
|
|
export const MODES = ['append', 'scan-first', 'review'];
|
||
|
|
const DEFAULT_MODE = 'append';
|
||
|
|
const GLOBAL_KEY = 'publish_mode_global';
|
||
|
|
const perCatKey = (slug) => `publish_mode_${slug}`;
|
||
|
|
|
||
|
|
export function globalMode() {
|
||
|
|
const v = getSetting(GLOBAL_KEY, DEFAULT_MODE);
|
||
|
|
return MODES.includes(v) ? v : DEFAULT_MODE;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function categoryMode(slug) {
|
||
|
|
const v = getSetting(perCatKey(slug), null);
|
||
|
|
if (v && MODES.includes(v)) return { mode: v, source: 'category' };
|
||
|
|
return { mode: globalMode(), source: 'global' };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function allModes(categories) {
|
||
|
|
const g = globalMode();
|
||
|
|
const out = { global: g, categories: {} };
|
||
|
|
for (const slug of categories) {
|
||
|
|
const v = getSetting(perCatKey(slug), null);
|
||
|
|
out.categories[slug] = v && MODES.includes(v) ? v : null; // null = inherits global
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set a mode (global or per-category). `category=null` sets the global default;
|
||
|
|
* a slug sets that category. Writing an empty value for a category clears the
|
||
|
|
* override and the category falls back to the global default.
|
||
|
|
*/
|
||
|
|
export function setMode({ category, value, actor }) {
|
||
|
|
const key = category ? perCatKey(category) : GLOBAL_KEY;
|
||
|
|
const now = new Date().toISOString();
|
||
|
|
|
||
|
|
if (!value) {
|
||
|
|
// Clearing: delete the row by writing a sentinel and relying on the admin
|
||
|
|
// page to show "inherits global". Simpler: just set the global default
|
||
|
|
// string so getSetting returns the default. For an override we need an
|
||
|
|
// actual delete — add it as a direct exec since it's one path.
|
||
|
|
deleteSetting(key, actor, now, category);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!MODES.includes(value)) {
|
||
|
|
throw new Error(`unknown mode: ${value}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const prev = getSetting(key, category ? null : DEFAULT_MODE);
|
||
|
|
setSetting(key, value, actor, now);
|
||
|
|
logAudit({
|
||
|
|
actor,
|
||
|
|
action: 'set_mode',
|
||
|
|
target: category || 'global',
|
||
|
|
details: { before: prev, after: value },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The one-off path for clearing a per-category override. */
|
||
|
|
function deleteSetting(key, actor, now, category) {
|
||
|
|
// We don't ship a dedicated delete-setting helper — this is the only caller.
|
||
|
|
// Importing the raw db just for this would be a dependency inversion; use
|
||
|
|
// the public API: write a reserved sentinel, then the resolver ignores it.
|
||
|
|
// Easier: just set it to the empty string and have the reader fall through.
|
||
|
|
const prev = getSetting(key, null);
|
||
|
|
setSetting(key, '', actor, now);
|
||
|
|
logAudit({
|
||
|
|
actor,
|
||
|
|
action: 'clear_mode',
|
||
|
|
target: category || 'global',
|
||
|
|
details: { before: prev, after: null },
|
||
|
|
});
|
||
|
|
}
|