Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/llm_fallback.txt
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,11 @@ Current limits (source values):
Zip scan and signing (preview, CLI v0.2):
- `vapour scan <zip|dir>` (also `--dir`, `--json`) checks a build locally; exit 1 on any error finding. It writes no attestation.
- `vapour verify <zip> [--attestation <file>] [--json]` checks a zip against its attestation locally.
- `vapour publish` (including `--dry-run`) runs the scan, signs the zip and writes `.vapour/build.zip.attestation.json`. There is no flag to skip the scan. With `--experimental-chunks` the scan runs but no attestation is sent.
- `vapour publish` (including `--dry-run`) scans, signs and locally verifies the exact ZIP; it writes `.vapour/build.zip.attestation.json`. There is no flag to skip verification. `--experimental-chunks` supports dry runs but cannot upload without an attestation.
- `vapour verify <zip> --remote --version <semver>` also compares the server's recorded SHA-256 and status; exit 1 unless the status is `verified`.
- `vapour verify --remote --version <semver>` may omit the ZIP: it checks the signed attestation and server record, but not local ZIP bytes. `--dir` selects the project and `--json` emits a result object.
- Server verification is asynchronous: the upload is accepted right away and the server records `pending`, then `verified`, `rejected` or `unverified`. `unverified` means no attestation was sent (older CLI) and the server scan passed.
- The server first requires and independently verifies the signed attestation, registered developer key, ZIP SHA-256 and size before storing a version. Missing signatures return 400 `verification_required`; forged, mismatched or wrong-key signatures return 400 `verification_failed`. Malformed, duplicate or oversized attestations return 400 `invalid_upload`.
- The independent archive scan is asynchronous after accepted uploads: the server records `pending`, then `verified` or `rejected`. Legacy versions or queue failures can still appear as `unverified`; new unsigned ZIP uploads are not accepted.
- Reject reasons include `bad_signature`, `hash_mismatch`, `size_mismatch`, `bad_key_id`, `bad_key`, `unsupported_version`, `unknown_key`, `bad_attestation`, or any scan code. A rejected version is taken offline and can never be approved; publish a new version.
- Malformed requests (invalid attestation JSON, attestation over 64 KiB) fail immediately with HTTP 400 `invalid_upload`.
- The first valid signing key a developer account uses is remembered; a zip signed by a different key is rejected with `unknown_key`. Losing the key requires a platform-team reset.
Expand Down
13 changes: 13 additions & 0 deletions cmd/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/vapour-computer/vapour-cli/internal/project"
"github.com/vapour-computer/vapour-cli/internal/storepage"
"github.com/vapour-computer/vapour-cli/internal/zipdir"
"github.com/vapour-computer/vapour-cli/internal/zipsign"
"github.com/vapour-computer/vapour-cli/pkg/delivery"
)

Expand Down Expand Up @@ -67,6 +68,9 @@ Release notes are required with --notes or --notes-file.`,
return err
}
if chunked {
if !dryRun {
return errors.New("--experimental-chunks cannot upload without a signed build attestation; use ZIP publishing")
}
if err := scanFolder(out, buildPath); err != nil {
return err
}
Expand Down Expand Up @@ -160,6 +164,15 @@ Release notes are required with --notes or --notes-file.`,
if err != nil {
return err
}
// Use the same local verification as `vapour verify` before any
// network call. The API independently verifies the signed bytes.
att, err := zipsign.Parse(attestation)
if err != nil {
return fmt.Errorf("cannot upload an unverified build: %w", err)
}
if err := zipsign.Verify(zipPath, att); err != nil {
return fmt.Errorf("cannot upload an unverified build: %w", err)
}
printBundle(out, bundle)
meta, err := publishMetadata(m, dir)
if err != nil {
Expand Down
12 changes: 12 additions & 0 deletions cmd/zipcheck_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,18 @@ func TestPublishUploadsAttestationAndBuild(t *testing.T) {
}
}

func TestChunkUploadCannotBypassVerificationGate(t *testing.T) {
api := zipEnv(t)
dir := writeFolderProject(t)
out, err := run(t, "publish", "--dir", dir, "--version", "0.1.0", "--notes", "x", "--skip-store", "--experimental-chunks")
if err == nil || !strings.Contains(err.Error(), "cannot upload without a signed build attestation") {
t.Fatalf("chunk upload should be blocked: err=%v out=%q", err, out)
}
if api.count() != 0 {
t.Fatalf("chunk upload reached server: %d request(s)", api.count())
}
}

func TestVerifyLocalZipAndTampering(t *testing.T) {
t.Run("happy path", func(t *testing.T) {
zipEnv(t)
Expand Down
12 changes: 8 additions & 4 deletions docs/llm/publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Flags: `--dir`, required `--version`, exactly one of `--notes` or `--notes-file`

Order for default ZIP publishing: load + validate manifest → load store bundle (unless `--skip-store`) → resolve release notes
(`--notes-file` or `--notes`, non-empty after trimming; ≤5000 chars) → pack the build → scan the ZIP → sign it and write
`.vapour/build.zip.attestation.json` → (stop here on `--dry-run`) → register project if no id → push store page → upload
`.vapour/build.zip.attestation.json` → verify the signed ZIP locally → (stop here on `--dry-run`) → register project if no id → push store page → upload
build and attestation. Store goes first so rejected media doesn't use up a version. The server verifies the ZIP, then an
admin reviews the version before it goes live.

Expand All @@ -26,6 +26,10 @@ Unless suppressed by `--no-perf-hints`, command output also includes warnings fr
A version can be published once: API error `version_exists` becomes "pass a new --version".
Success prints the game URL.

`--experimental-chunks`: opt-in content-addressed chunk protocol (`pkg/delivery/README.md`); the deployed API doesn't
implement it yet. Default remains ZIP. The chunk path scans but does not send a signed ZIP attestation or release notes.
Upload chunk URLs must be HTTPS and on an allowlisted public host.
`--experimental-chunks`: its dry run still scans, but real chunk uploads are blocked until this protocol can provide
a signed attestation that the API can verify. Use ZIP publishing for uploads. The deployed API does not implement the
chunk protocol yet. Upload chunk URLs must be HTTPS and on an allowlisted public host.

The API requires an attestation on every ZIP upload and verifies its Ed25519 signature, key identity, ZIP SHA-256 and
size before writing the build. Missing or forged attestations fail with `verification_required` or `verification_failed`.
The CLI's local check does not replace the server's check.
Loading