Files
Shade/docs/cross-platform.md
Sterister e6fdf31b49
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
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
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>
2026-05-03 18:35:35 +02:00

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:

  1. How the parity gate works (CI)
  2. How to run vectors locally
  3. 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 jobsts-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 a description per 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.