Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers byte-for-byte. The version bump is semantic: audit-cycle complete, opt-in surface fully exposed, threat model refreshed for every new surface. Highlights: - All 24 @shade/* packages bumped to 4.0.0 in lockstep. - CHANGELOG 4.0.0 section is the canonical manifest of what landed. - THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12 Web-Worker boundary) + residual-risks table refreshed. - OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox, bridge, observer, /metrics, /healthz, /ready. - MIGRATION 0.3.x → 4.0 documented + smoke-tested against shade migrate-storage on a real SQLite DB. - docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer. - scripts/soak.ts harness for the GA-stable 2-week soak window. - All V*.md plans archived under docs/archive/ with Status: Done. - Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen non-realtime stack. Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green. Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports version 4.0.0 on /health. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
281 lines
9.0 KiB
TypeScript
281 lines
9.0 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
||
import { ValidationError } from '@shade/core';
|
||
import {
|
||
declaresThumbnail,
|
||
encodeStreamControl,
|
||
isAllowedThumbnailMime,
|
||
mainStreamIdForThumbnail,
|
||
parseStreamControl,
|
||
THUMBNAIL_MAX_BYTES,
|
||
THUMBNAIL_MIME_ALLOWLIST,
|
||
thumbnailStreamIdFor,
|
||
validateFileMetadata,
|
||
} from '../src/index.js';
|
||
import type { StreamFileMetadata, StreamInitMessage } from '../src/index.js';
|
||
|
||
describe('V3.9 fileMetadata — schema', () => {
|
||
test('THUMBNAIL_MAX_BYTES is 64 KiB', () => {
|
||
expect(THUMBNAIL_MAX_BYTES).toBe(64 * 1024);
|
||
});
|
||
|
||
test('THUMBNAIL_MIME_ALLOWLIST is jpeg/webp/png only', () => {
|
||
expect([...THUMBNAIL_MIME_ALLOWLIST]).toEqual(['image/jpeg', 'image/webp', 'image/png']);
|
||
});
|
||
|
||
test('isAllowedThumbnailMime accepts allowlist + rejects others', () => {
|
||
for (const m of THUMBNAIL_MIME_ALLOWLIST) expect(isAllowedThumbnailMime(m)).toBe(true);
|
||
expect(isAllowedThumbnailMime('image/svg+xml')).toBe(false);
|
||
expect(isAllowedThumbnailMime('image/avif')).toBe(false);
|
||
expect(isAllowedThumbnailMime('text/html')).toBe(false);
|
||
expect(isAllowedThumbnailMime('')).toBe(false);
|
||
});
|
||
|
||
test('thumbnailStreamIdFor + mainStreamIdForThumbnail roundtrip', () => {
|
||
const main = 'AAAAAAAAAAAAAAAAAAAAAA';
|
||
const thumb = thumbnailStreamIdFor(main);
|
||
expect(thumb).toBe(`${main}.thumb`);
|
||
expect(mainStreamIdForThumbnail(thumb)).toBe(main);
|
||
expect(mainStreamIdForThumbnail(main)).toBeNull();
|
||
});
|
||
|
||
test('declaresThumbnail requires hash + mime + bytes + streamId', () => {
|
||
expect(declaresThumbnail(undefined)).toBe(false);
|
||
expect(declaresThumbnail({})).toBe(false);
|
||
expect(declaresThumbnail({ thumbnailHash: 'x' })).toBe(false);
|
||
expect(
|
||
declaresThumbnail({
|
||
thumbnailHash: 'x',
|
||
thumbnailMime: 'image/webp',
|
||
thumbnailBytes: 100,
|
||
}),
|
||
).toBe(false);
|
||
expect(
|
||
declaresThumbnail({
|
||
thumbnailHash: 'x',
|
||
thumbnailMime: 'image/webp',
|
||
thumbnailBytes: 100,
|
||
thumbnailStreamId: 'BBBB',
|
||
}),
|
||
).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('V3.9 validateFileMetadata', () => {
|
||
test('accepts empty object', () => {
|
||
expect(() => validateFileMetadata({})).not.toThrow();
|
||
});
|
||
|
||
test('accepts a fully-populated metadata', () => {
|
||
const meta: StreamFileMetadata = {
|
||
filename: 'report.pdf',
|
||
mimeType: 'application/pdf',
|
||
thumbnailStreamId: 'BB',
|
||
thumbnailHash: 'aGFzaA==',
|
||
thumbnailMime: 'image/webp',
|
||
thumbnailBytes: 12345,
|
||
};
|
||
expect(() => validateFileMetadata(meta)).not.toThrow();
|
||
});
|
||
|
||
test('rejects oversized thumbnailBytes', () => {
|
||
expect(() =>
|
||
validateFileMetadata({
|
||
thumbnailMime: 'image/webp',
|
||
thumbnailBytes: THUMBNAIL_MAX_BYTES + 1,
|
||
}),
|
||
).toThrow(ValidationError);
|
||
});
|
||
|
||
test('rejects negative or non-integer thumbnailBytes', () => {
|
||
expect(() =>
|
||
validateFileMetadata({ thumbnailMime: 'image/webp', thumbnailBytes: -1 }),
|
||
).toThrow(ValidationError);
|
||
expect(() =>
|
||
validateFileMetadata({ thumbnailMime: 'image/webp', thumbnailBytes: 1.5 }),
|
||
).toThrow(ValidationError);
|
||
});
|
||
|
||
test('rejects thumbnailMime outside allowlist', () => {
|
||
expect(() =>
|
||
validateFileMetadata({
|
||
thumbnailMime: 'image/svg+xml' as never,
|
||
thumbnailBytes: 1024,
|
||
}),
|
||
).toThrow(ValidationError);
|
||
});
|
||
|
||
test('rejects thumbnailMime declared without thumbnailBytes', () => {
|
||
expect(() =>
|
||
validateFileMetadata({ thumbnailMime: 'image/webp' }),
|
||
).toThrow(ValidationError);
|
||
});
|
||
|
||
test('rejects filename with control characters', () => {
|
||
expect(() => validateFileMetadata({ filename: 'foo\nbar' })).toThrow(ValidationError);
|
||
expect(() => validateFileMetadata({ filename: 'foo\rbar' })).toThrow(ValidationError);
|
||
expect(() => validateFileMetadata({ filename: 'foo |