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>
7.0 KiB
Cross-platform parity — adding & running vectors
Shade keeps its TypeScript and Kotlin implementations in lock-step via a
single source of truth: test-vectors/*.json. Both runners load the
same files and verify their native code produces byte-identical output.
This document covers:
- How the parity gate works (CI)
- How to run vectors locally
- How to add a new vector
How the gate works
┌─────────────────────────────────┐
│ scripts/generate-vectors.ts │
│ (TS reference implementation) │
└────────────────┬────────────────┘
│ writes
▼
┌─────────────────────────────────┐
│ test-vectors/*.json │
│ { version: 2, vectors: [...] }│
└─────┬──────────────────┬────────┘
│ │
│ loaded by │ loaded by
▼ ▼
┌───────────────────────────┐ ┌───────────────────────────┐
│ packages/shade-core/ │ │ android/shade-android/ │
│ tests/cross-platform- │ │ src/test/kotlin/.../ │
│ vectors.test.ts │ │ CrossPlatformVectorTest │
│ (bun) │ │ (gradle JUnit4) │
└───────────────────────────┘ └───────────────────────────┘
│ │
└─────────┬────────┘
▼
both must pass before merge
(.gitea/workflows/cross-vectors.yml)
The CI workflow has two independent jobs — ts-vectors and
kotlin-vectors. Either failing blocks the merge. The TS job also runs
bun run vectors:gen and fails if the result diverges from the committed
files: vector commits must come from the generator, never hand edits.
Vector files have a version integer at the top. Bump
VECTOR_FILE_VERSION in scripts/generate-vectors.ts whenever the
schema of any vector file changes (not just the values). Both test
suites assert the version matches their hard-coded expectation.
Running vectors locally
TypeScript
bun run test:vectors
# under the hood:
# bun test packages/shade-core/tests/cross-platform-vectors.test.ts
Kotlin (JVM, no Android SDK required)
cd android
./gradlew :shade-android:test
Requires JDK 17. The wrapper downloads Gradle 8.10.2 on first run. Tink 1.15.0 (JVM JAR) is pulled from Maven Central.
Regenerating vectors
When the protocol changes (new wire field, new label, new derivation step)
the TS reference is the source of truth. Edit generate-vectors.ts, then:
bun run vectors:gen
git diff test-vectors/ # eyeball the change
bun run test:vectors # confirm TS still agrees
cd android && ./gradlew :shade-android:test # confirm Kotlin still agrees
If Kotlin disagrees, fix Kotlin — TS is canonical. If both agree but the diff is unintentional (e.g. you added a field by accident), revert the generator change.
Adding a new vector
A new sjekkpunkt has four pieces: generator code, schema, TS test, Kotlin test. All four must land in the same PR; otherwise the gate trips on the missing half.
Step 1 — Add a generator function
In scripts/generate-vectors.ts, add a function that:
- Takes deterministic inputs (no randomness — fix every byte)
- Computes the value via the TS reference primitives
- Returns a
Vector[]with adescriptionper case + all inputs and outputs in hex
Example skeleton:
async function generateMyVectors(): Promise<Vector[]> {
const input = new Uint8Array(32).fill(0xab);
const output = await someRefImpl(input);
return [{
description: 'My new sjekkpunkt: known input → known output',
input: hex(input),
output: hex(output),
}];
}
Wire it up in main():
['my-vectors.json', { vectors: await generateMyVectors() }],
Run bun run vectors:gen → you should see ✓ my-vectors.json and a new
file appears under test-vectors/.
Step 2 — Add a TS test
In packages/shade-core/tests/cross-platform-vectors.test.ts:
test('My vectors match', async () => {
const { vectors } = loadVectors('my-vectors.json');
for (const v of vectors) {
const actual = await someRefImpl(fromHex(v.input));
expect(hex(actual)).toBe(v.output);
}
});
loadVectors already asserts the version field matches. If you're
introducing a schema-breaking change, bump EXPECTED_VECTOR_VERSION and
VECTOR_FILE_VERSION together.
Step 3 — Add the Kotlin equivalent
In
android/shade-android/src/test/kotlin/no/zyon/shade/CrossPlatformVectorTest.kt:
@Test
fun myVectorsMatch() {
val vectors = loadVectors("my-vectors.json")
for (i in 0 until vectors.length()) {
val v = vectors.getJSONObject(i)
val actual = someKotlinImpl(fromHex(v.getString("input")))
assertEquals(v.getString("output"), hex(actual))
}
}
If the Kotlin port doesn't yet have someKotlinImpl, that's the implementation
work the new vector is gating — write it and re-run the test until it passes.
Step 4 — Verify the gate trips on divergence
Sanity check: temporarily flip a byte in your Kotlin port and run
./gradlew :shade-android:test. The test should fail within 60 seconds
(see docs/V3.5.md §Akseptansekriterier). Revert.
Why a separate generator (vs. golden fixtures)?
Golden test fixtures rot — when the protocol changes, every test file
that pinned a literal hex string needs updating, and it's easy to
"update" Kotlin to match a stale TS-generated value. By centralising
vector generation in one TS script, the protocol changes in one
place (the reference impl + generate-vectors.ts), the file
regenerates with one command, and any platform that drifts gets caught
by the next CI run.
Schema versioning
{ "version": 2, "vectors": [...] } is the file format. Bump the int
when the shape of any vector changes (e.g. you add a field consumers
must read). Both runners hard-code their expected version and refuse to
parse mismatched files — this catches the case where a new vector field
was added in TS but the Kotlin loader silently ignored it.
Schema changes go in the same PR as the bump + the matching loader update on both sides.