Skip to content
Merged
93 changes: 93 additions & 0 deletions .github/workflows/cargo-cache-seed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# `ci.yml` runs on `pull_request` only, and GitHub scopes a cache entry to the
# ref that wrote it — a PR writes under `refs/pull/N/merge`, invisible to every
# other PR, so each PR's first cargo run is cold. A run can also read entries
# scoped to the default branch, which is what this workflow writes.
#
# Two entries, not one per cargo job: the per-job keys stay each PR's own save
# key and gain a trailing `main-cargo-` / `main-wasm-cargo-` restore-key that
# reaches these. The keys hash `Cargo.lock` and the toolchain — the two inputs
# that invalidate a dependency tree — so a merge that changes neither hits its
# own key and writes nothing. The repo's cache budget is already tight, and
# these entries have to earn their bytes against the PR-scoped ones LRU evicts.
name: Cargo Cache Seed

on:
push:
branches: [main]
paths:
- 'Cargo.lock'
- 'rust-toolchain.toml'
- '.github/workflows/cargo-cache-seed.yml'
# An entry LRU evicted between lockfile changes has no push to recreate it.
schedule:
- cron: '0 5 * * 1'
workflow_dispatch:

permissions: {}

concurrency:
group: cargo-cache-seed
cancel-in-progress: true

jobs:
# Serves every job keyed `linux-cargo-`, `core-kats-cargo-` and
# `client-browser-cargo-`: one host `target` tree and one registry.
native:
name: Seed the native Linux cargo cache
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: main-cargo-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: main-cargo-

# Compiled, not run: what a PR restores this for is the dependency tree.
# The desktop shell stays out — Tauri needs webkit2gtk system deps, and
# its own jobs cache under their own keys.
- name: Build the test targets the PR cargo jobs compile
run: cargo test --workspace --exclude cipherbox-desktop --no-run

# Serves the `wasm-engine-cargo-` and `web-e2e-cargo-` jobs, whose path set is
# the host build tree plus the browser target rather than the whole of `target`.
wasm:
name: Seed the wasm cargo cache
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
target/release
target/wasm32-unknown-unknown
key: main-wasm-cargo-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: main-wasm-cargo-

- uses: ./.github/actions/wasm-bindgen-cli

- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: 'package.json'

- name: Build the engine WASM
run: pnpm --filter @cipherbox/web run build:wasm
28 changes: 21 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,9 @@ jobs:
target/release
target/wasm32-unknown-unknown
key: wasm-engine-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: wasm-engine-cargo-
restore-keys: |
wasm-engine-cargo-
main-wasm-cargo-
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- uses: ./.github/actions/wasm-bindgen-cli

Expand Down Expand Up @@ -473,7 +475,9 @@ jobs:
~/.cargo/git
target
key: linux-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: linux-cargo-
restore-keys: |
linux-cargo-
main-cargo-

- name: Cargo fmt
run: cargo fmt --all --check
Expand Down Expand Up @@ -508,7 +512,9 @@ jobs:
~/.cargo/git
target
key: linux-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: linux-cargo-
restore-keys: |
linux-cargo-
main-cargo-

- name: Engine KAT vectors fresh (committed generator is the only writer)
run: |
Expand Down Expand Up @@ -543,7 +549,9 @@ jobs:
~/.cargo/git
target
key: linux-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: linux-cargo-
restore-keys: |
linux-cargo-
main-cargo-

- name: FUSE operation-core suite
run: cargo test -p cipherbox-fuse
Expand Down Expand Up @@ -573,7 +581,9 @@ jobs:
~/.cargo/git
target
key: core-kats-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: core-kats-cargo-
restore-keys: |
core-kats-cargo-
main-cargo-

# wasm-bindgen-test-runner drives the wasm32-unknown-unknown legs in Node.
- uses: ./.github/actions/wasm-bindgen-cli
Expand Down Expand Up @@ -662,7 +672,9 @@ jobs:
~/.cargo/git
target
key: client-browser-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: client-browser-cargo-
restore-keys: |
client-browser-cargo-
main-cargo-

# wasm-bindgen-cli drives the conformance module build.
- uses: ./.github/actions/wasm-bindgen-cli
Expand Down Expand Up @@ -824,7 +836,9 @@ jobs:
~/.cargo/git
target
key: linux-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: linux-cargo-
restore-keys: |
linux-cargo-
main-cargo-

- name: Install dependencies
run: pnpm install --frozen-lockfile
Expand Down
24 changes: 23 additions & 1 deletion .github/workflows/web-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ jobs:
--health-interval 10s
--health-timeout 5s
--health-retries 5
# The write path's block store. Without it `KuboPinStore` refuses every
# hosted upload with a 503, which the drain charges as a spent attempt —
# so a write spec dead-letters rather than merely failing to publish.
ipfs:
image: ipfs/kubo:v0.42.0
ports:
- 5001:5001
- 8080:8080
options: >-
--health-cmd "ipfs id"
--health-interval 10s
--health-timeout 5s
--health-retries 10
--health-start-period 30s

env:
DB_HOST: localhost
Expand All @@ -57,6 +71,7 @@ jobs:
DB_PASSWORD: postgres
DB_DATABASE: cipherbox_test
JWT_SECRET: web-e2e-jwt-secret
KUBO_API_URL: http://localhost:5001
# Every test cold-starts its own account from one runner IP, so the
# per-IP auth bucket would otherwise cap how many specs can exist.
THROTTLE_AUTH_LIMIT: '200'
Expand All @@ -73,6 +88,11 @@ jobs:
# The hermetic record store, in place of the public default — a merge gate
# must not resolve vault pointers over the internet.
VITE_ROUTING_ENDPOINTS: http://localhost:3001
# The same stack's gateway, so a read pulls its blocks back from where the
# upload pinned them rather than from the tab that wrote them. By address,
# not by name: Kubo serves `localhost` as a *subdomain* gateway and 301s
# every path request to `<cid>.ipfs.localhost`, which resolves nowhere.
VITE_READ_ACCELERATOR_URL: http://127.0.0.1:8080

steps:
- name: Checkout code
Expand All @@ -97,7 +117,9 @@ jobs:
target/release
target/wasm32-unknown-unknown
key: web-e2e-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: web-e2e-cargo-
restore-keys: |
web-e2e-cargo-
main-wasm-cargo-

- uses: ./.github/actions/wasm-bindgen-cli
if: inputs.wasm-artifact == ''
Expand Down
22 changes: 22 additions & 0 deletions apps/web/src/engine/introspection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,28 @@ describe('installIntrospection', () => {
expect((await pending)?.settled).toBe(false);
});

it('reads a node named in hex and hands its plaintext back the same way', async () => {
const engine = fakeEngine();
const download = vi.fn().mockResolvedValue(Uint8Array.of(0xde, 0xad, 0xbe, 0xef).buffer);
(engine.client.facade as unknown as { download: unknown }).download = download;
installIntrospection(engine.client);

const plaintext = await window.__CIPHERBOX_ENGINE__?.download('0102030405060708');

expect(download).toHaveBeenCalledWith(Uint8Array.of(1, 2, 3, 4, 5, 6, 7, 8));
expect(plaintext).toBe('deadbeef');
});

it.each(['010', 'zz'])('refuses %s as a node id, before the engine is asked', async (bad) => {
const engine = fakeEngine();
const download = vi.fn();
(engine.client.facade as unknown as { download: unknown }).download = download;
installIntrospection(engine.client);

await expect(window.__CIPHERBOX_ENGINE__?.download(bad)).rejects.toThrow(TypeError);
expect(download).not.toHaveBeenCalled();
});

it('records the event stream in emission order', () => {
const engine = fakeEngine();
installIntrospection(engine.client);
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/engine/introspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* hook from is the very one under test.
*/

import { toHex } from '@cipherbox/client';
import { fromHex, toHex } from '@cipherbox/client';
import { handOffLoginSecret } from '@cipherbox/login';
import type { EngineClient, EventDescriptor, SnapshotDescriptor } from '@cipherbox/client';
import { authStore } from '../stores/auth.store';
Expand Down Expand Up @@ -39,6 +39,8 @@ export interface EngineIntrospection {
signIn(loginSecretHex: string): Promise<void>;
/** The engine's view of the vault root. */
snapshot(): Promise<IntrospectedView>;
/** One node's plaintext as the engine reads it back, hex like every other tap. */
download(nodeHex: string): Promise<string>;
/** Every engine event this tab has seen, in emission order. */
events(): Plain<EventDescriptor>[];
}
Expand Down Expand Up @@ -72,6 +74,9 @@ export function installIntrospection(client: EngineClient): EngineClient {
const view = await client.facade.snapshot(null);
return { view: plain(view) as Plain<SnapshotDescriptor>, settled: settled(view) };
},
async download(nodeHex) {
return toHex(new Uint8Array(await client.facade.download(fromHex(nodeHex))));
},
events: () => seen,
};
return client;
Expand Down
Loading