feat(media): allow HTML/Markdown/CSV uploads; serve scriptable types as downloads

- media upload policy now allows text/html, text/markdown, text/x-markdown,
  text/csv (in addition to images/av, pdf, txt, zip, office docs)
- blob endpoint adds X-Content-Type-Options: nosniff, and forces
  Content-Disposition: attachment for script-capable types (html, xhtml, svg,
  xml) so an uploaded file can't render/execute inline from the IIOS origin
  (stored-XSS). Images/video/audio/pdf still serve inline for preview.
- dev-opa test covering the allowed types + unknown/oversize denials

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 14:28:31 +05:30
parent 32aa04503d
commit b4104b9769
3 changed files with 23 additions and 1 deletions
@@ -49,6 +49,19 @@ describe('DevOpaPort (dev policy plane — membership rules)', () => {
expect((await opa.decide({ action: 'iios.thread.participant.remove', callerRole: 'MEMBER' })).allow).toBe(true);
});
it('media upload allows images + docs (incl. html/markdown/csv), denies unknown types and oversize', async () => {
const up = (mime: string, sizeBytes = 1024) => opa.decide({ action: 'iios.media.upload', mime, sizeBytes });
for (const mime of ['image/png', 'video/mp4', 'audio/mpeg', 'application/pdf', 'text/plain', 'text/markdown', 'text/html', 'text/csv']) {
expect((await up(mime)).allow).toBe(true);
}
const bad = await up('application/x-msdownload');
expect(bad.allow).toBe(false);
expect(bad.obligations[0]?.reason).toMatch(/not allowed/);
const big = await up('image/png', 30 * 1024 * 1024);
expect(big.allow).toBe(false);
expect(big.obligations[0]?.reason).toMatch(/too large/);
});
it('self-join is governed only on membership threads', async () => {
// generic / support thread (no membership attr) → open join, unchanged
expect((await opa.decide({ action: 'iios.thread.join', alreadyMember: false })).allow).toBe(true);