S4 (a1): the media engine — VOICE templates, /desk, Ghost drafts
4a VOICE v1.1 committed (board start); lint is EXECUTABLE — banned list + no-exclamation law enforced in the engine (throws in test, drops in prod) and locked by tests. Curly-apostrophe variants covered. 4b mediaEngine: deterministic templates (MORNING WIRE, SIGNAL, STREAK WATCH, THE SETTLE, RECEIPTS, ARCHETYPE WATCH, LINE DISPATCH) filled ONLY from snapshot/ledger/streaks JSON. Record percentages never render under n>=20 (counts + 'Record building' below). Stark layer = curated committed library (content/stark-lines.json), day-rotated selection — selected, never generated. 4c /desk (founder-only: requireAuth + DESK_OWNERS email allowlist, deny-by-default): all formats as text + <=280-char pre-segmented tweets with per-tweet copy buttons + char counts, wire/numbers-only variants, DATA BRIEF block (structured day numbers) with copy-for-claude.ai. ntfy ping after the day's first snapshot: 'Desk pack ready'. 4d ghostPublisher: DRAFTS ONLY (status:'draft' test-locked), env-gated no-op, HS256 JWT via node crypto (zero new deps). POST /api/internal/ghost/drafts saves slate preview + settle drafts. Nothing anywhere auto-posts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* /api/desk (Session 63 / A1-S4) — Kev's copy-paste arsenal.
|
||||
*
|
||||
* GET /pack — the assembled daily media pack + DATA BRIEF.
|
||||
* Auth: requireAuth + email allowlist (env DESK_OWNERS, comma-separated).
|
||||
* Unset allowlist → 403 for everyone (deny by default; the desk is the
|
||||
* founder's surface, not a tier feature).
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const { createRateLimit } = require('../middleware/rateLimit');
|
||||
const { assembleDeskPack } = require('../services/deskService');
|
||||
|
||||
const router = express.Router();
|
||||
router.use(createRateLimit({ windowMs: 60_000, max: 20 }));
|
||||
|
||||
function isOwner(req) {
|
||||
const owners = String(process.env.DESK_OWNERS || '')
|
||||
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
|
||||
const email = String(req.user?.email || '').toLowerCase();
|
||||
return owners.length > 0 && email && owners.includes(email);
|
||||
}
|
||||
|
||||
router.get('/pack', requireAuth, async (req, res) => {
|
||||
if (!isOwner(req)) {
|
||||
return res.status(403).json({ error: 'The desk is the publisher’s surface. Set DESK_OWNERS to grant access.' });
|
||||
}
|
||||
try {
|
||||
const pack = await assembleDeskPack();
|
||||
res.set('Cache-Control', 'private, max-age=60');
|
||||
return res.json(pack);
|
||||
} catch (err) {
|
||||
console.error('[desk/pack]', err.message);
|
||||
return res.status(200).json({ generated_at: null, formats: {}, data_brief: null, error: 'assembly failed' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -180,6 +180,25 @@ router.post('/outcomes/all', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/internal/ghost/drafts (Session 63 / A1-S4d) — assemble today's
|
||||
* desk pack and save the slate-preview + settle drafts to Ghost. DRAFTS
|
||||
* ONLY; Kev publishes from the Ghost admin. Env-gated no-op without Ghost.
|
||||
*/
|
||||
router.post('/ghost/drafts', async (req, res) => {
|
||||
try {
|
||||
const { assembleDeskPack } = require('../services/deskService');
|
||||
const ghost = require('../services/ghostPublisher');
|
||||
const pack = await assembleDeskPack();
|
||||
const result = await ghost.saveDailyDrafts(pack);
|
||||
return res.json({ ok: result.ok, result });
|
||||
} catch (err) {
|
||||
const message = err && err.message ? err.message : String(err);
|
||||
console.error('[internal/ghost/drafts] failed:', message);
|
||||
return res.status(500).json({ ok: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/internal/refresh/all (Session 60, Phase 2.5) — manual intraday
|
||||
* odds-only refresh: STEAM/VALUE movement, public revisions, closing
|
||||
|
||||
Reference in New Issue
Block a user