roadmap.md
1 # Auths v0.1.0 Engineering Roadmap 2 3 Zero users. No backward compatibility constraints. Break anything, delete anything. 4 5 ## Priority Order 6 7 | # | Epic | What | Blocked by | 8 |---|------|------|------------| 9 | 1 | Epic 1 | Wire Rust commit verification into Python | — | DONE 10 | 2 | Epic 2 | Replace all string-matching error dispatch | — | 11 | 3 | Epic 3 | Fix known bugs and code violations | — | 12 | 4 | Epic 4 | Lock down public API surface | — | 13 | 5 | Epic 5 | Typestate PassphraseProvider | — | 14 | 6 | Epic 6 | Python wheels for all platforms | — | 15 | 7 | Epic 7 | Decouple widget from local filesystem | — | 16 | 8 | Epic 8 | GitHub Action cleanup | — | 17 | 9 | Epic 9 | Python SDK types | Epics 1, 2 | 18 | 10 | Epic 10 | UMD bundle for CDN | Epic 7 | 19 | 11 | Epic 11 | Getting started guide | All above | 20 21 22 ## Epic 2 — Structured Error Codes Everywhere 23 24 Three places use string matching to classify errors. Replace all of them with `AuthsErrorInfo::error_code()` dispatch. 25 26 **Task 2.1** — Add `error_code: Option<String>` to `VerificationResult` in `packages/auths-python/src/types.rs` (currently only has `valid` and `error`). Propagate `e.error_code()` from all FFI error paths in `verify.rs` and `sign.rs`. 27 28 ```rust 29 // packages/auths-python/src/types.rs 30 31 #[pyclass] 32 #[derive(Clone)] 33 pub struct VerificationResult { 34 #[pyo3(get)] 35 pub valid: bool, 36 #[pyo3(get)] 37 pub error: Option<String>, 38 #[pyo3(get)] 39 pub error_code: Option<String>, 40 } 41 ``` 42 43 **Task 2.2** — Delete `_map_verify_error()`, `_map_sign_error()`, and `_map_network_error()` in `_client.py:30-56`. Replace with code-based dispatch. No fallback logic needed — there are no callers relying on the old behavior. 44 45 ```python 46 # packages/auths-python/python/auths/_client.py 47 48 _ERROR_CODE_MAP = { 49 "AUTHS_VERIFICATION_ERROR": ("invalid_signature", VerificationError), 50 "AUTHS_MISSING_CAPABILITY": ("missing_capability", VerificationError), 51 "AUTHS_CRYPTO_ERROR": ("invalid_key", CryptoError), 52 "AUTHS_DID_RESOLUTION_ERROR": ("invalid_key", CryptoError), 53 "AUTHS_INVALID_INPUT": ("invalid_signature", VerificationError), 54 "AUTHS_SERIALIZATION_ERROR": ("invalid_signature", VerificationError), 55 "AUTHS_BUNDLE_EXPIRED": ("expired_attestation", VerificationError), 56 "AUTHS_KEY_NOT_FOUND": ("key_not_found", CryptoError), 57 "AUTHS_INCORRECT_PASSPHRASE": ("signing_failed", CryptoError), 58 "AUTHS_SIGNING_FAILED": ("signing_failed", CryptoError), 59 } 60 61 def _map_error(exc: Exception) -> Exception: 62 code = getattr(exc, "error_code", None) 63 if code and code in _ERROR_CODE_MAP: 64 py_code, cls = _ERROR_CODE_MAP[code] 65 return cls(str(exc), code=py_code) 66 return VerificationError(str(exc), code="unknown") 67 ``` 68 69 **Task 2.3** — Fix C FFI string matching in `crates/auths-verifier/src/ffi.rs:206-224` (marked `TECH-DEBT(fn-33)`). Replace substring checks with `e.error_code()` match. 70 71 ```rust 72 // crates/auths-verifier/src/ffi.rs 73 74 fn attestation_error_to_code(e: &AttestationError) -> i32 { 75 match e.error_code() { 76 "AUTHS_VERIFICATION_ERROR" => 2, 77 "AUTHS_CRYPTO_ERROR" => 3, 78 "AUTHS_MISSING_CAPABILITY" => 4, 79 "AUTHS_INVALID_INPUT" => 5, 80 "AUTHS_SERIALIZATION_ERROR" => 6, 81 "AUTHS_BUNDLE_EXPIRED" => 7, 82 _ => -1, 83 } 84 } 85 ``` 86 87 --- 88 89 ## Epic 3 — Bug Fixes and Code Violations 90 91 Known bugs that must be fixed before v0.1.0. No epic overlap — these fell through the cracks. 92 93 **Task 3.1** — Fix `Utc::now()` violation in `crates/auths-core/src/witness/server.rs:492`. The `submit_event` handler calls `chrono::Utc::now()` directly. Change the function signature to accept `now: DateTime<Utc>` and have the caller inject it. 94 95 **Task 3.2** — Replace 7 `anyhow::anyhow!()` calls in `crates/auths-storage/src/git/identity_adapter.rs` (lines 127, 129, 132, 134, 172, 177, 184) with a `StorageError` thiserror enum. Layer 4 crate, `anyhow` is banned. 96 97 **Task 3.3** — Fix `__init__.py` import bug. `verify_chain_with_witnesses` is in `__all__` at `packages/auths-python/python/auths/__init__.py:70` but never imported from `_native` (lines 13-29). Add the import if the function exists in the native module, otherwise remove it from `__all__`. 98 99 --- 100 101 ## Epic 4 — Seal Public API Surface 102 103 Every `pub` symbol in a published crate is a commitment. Clean it up now while there's zero cost to breaking changes. 104 105 **Task 4.1** — Make `ports`, `presentation`, and `workflows` modules `pub(crate)` in `crates/auths-sdk/src/lib.rs` (currently `pub` at lines 36, 38, 50). No `#[doc(hidden)]` half-measure — just make them private. The `testing` module is already feature-gated, leave it. 106 107 ```rust 108 // crates/auths-sdk/src/lib.rs 109 110 pub mod setup; 111 pub mod device; 112 pub mod signing; 113 pub mod keys; 114 pub mod pairing; 115 pub mod audit; 116 pub mod context; 117 pub mod error; 118 pub mod result; 119 pub mod types; 120 121 pub(crate) mod ports; 122 pub(crate) mod presentation; 123 pub(crate) mod workflows; 124 125 #[cfg(any(test, feature = "test-utils"))] 126 pub mod testing; 127 ``` 128 129 **Task 4.2** — Delete `submit_registration()` and `bind_platform_claim()` stubs in `crates/auths-sdk/src/setup.rs:326-382`. Both always return `None`. They're dead code with no callers outside the SDK. Remove them and their call sites in `setup()`. 130 131 **Task 4.3** — Add `cargo-public-api` CI step for `auths-sdk`, `auths-verifier`, and `auths-core`. Check the baseline into `docs/public-api/` so API drift shows up in diffs. 132 133 --- 134 135 ## Epic 5 — Typestate PassphraseProvider 136 137 `NoopPassphraseProvider` at `crates/auths-sdk/src/context.rs:30` fails at runtime. Make it a compile-time error instead. 138 139 **Task 5.1** — Add a 7th typestate slot `PP` to `AuthsContextBuilder` (currently has 6 at `context.rs:129-133`). Split into `AuthsSigningContext` and `AuthsReadContext`. Delete `NoopPassphraseProvider` entirely. 140 141 ```rust 142 pub type AuthsSigningContext = AuthsContext<HasPassphrase>; 143 pub type AuthsReadContext = AuthsContext<NoPassphrase>; 144 145 pub struct HasPassphrase(Arc<dyn PassphraseProvider + Send + Sync>); 146 pub struct NoPassphrase; 147 ``` 148 149 **Task 5.2** — Update CLI factories in `crates/auths-cli/src/factories/` to build the right context type per command. Signing commands get `AuthsSigningContext`, read-only commands get `AuthsReadContext`. 150 151 **Task 5.3** — Extract shared `build_ffi_context()` in Python FFI. The same builder block is copy-pasted 6 times across `identity.rs`, `rotation.rs`, `device_ext.rs`, `artifact_sign.rs`, `commit_sign.rs`. One function, 6 call sites. 152 153 ```rust 154 // packages/auths-python/src/context_helper.rs 155 156 pub fn build_ffi_context( 157 repo_path: &str, 158 passphrase: Option<&str>, 159 ) -> Result<AuthsContext, anyhow::Error> { 160 let (backend, keychain, clock, id_storage, att_storage) = resolve_ffi_deps(repo_path)?; 161 let provider = resolve_passphrase(passphrase); 162 Ok(AuthsContext::builder() 163 .registry(backend) 164 .key_storage(keychain) 165 .clock(clock) 166 .identity_storage(id_storage) 167 .attestation_sink(att_storage.clone()) 168 .attestation_source(att_storage) 169 .passphrase_provider(provider) 170 .build()) 171 } 172 ``` 173 174 --- 175 176 ## Epic 6 — Cross-Platform Python Wheels 177 178 `pip install auths` must work without a Rust toolchain on macOS (x86_64/arm64), Linux (manylinux), and Windows (amd64). 179 180 **Task 6.1** — Add `publish-python.yml` workflow using `maturin-action` to build wheels for all platforms on tagged releases. 181 182 ```yaml 183 jobs: 184 build: 185 strategy: 186 matrix: 187 os: [ubuntu-latest, macos-latest, windows-latest] 188 target: [x86_64, aarch64] 189 exclude: 190 - os: windows-latest 191 target: aarch64 192 steps: 193 - uses: PyO3/maturin-action@v1 194 with: 195 command: build 196 args: --release -m packages/auths-python/Cargo.toml --out dist 197 target: ${{ matrix.target }} 198 ``` 199 200 **Task 6.2** — Add smoke test: install the wheel in a clean venv (no Rust), run `from auths import Auths`, and test the `[jwt]` extra since `PyJWT`/`cryptography` have their own platform build issues. 201 202 --- 203 204 ## Epic 7 — Decouple Widget from Local Filesystem 205 206 The widget can't be built or published without the exact sibling directory layout. Fix that, plus the build bugs found during review. 207 208 **Task 7.1** — Publish `@auths/verifier-wasm` as a standalone npm package from `crates/auths-verifier` WASM build. Add `publish-wasm.yml` CI workflow on tagged releases. 209 210 **Task 7.2** — Replace `file:../auths/packages/auths-verifier-ts` devDependency and `build:wasm` script in `auths-verify-widget/package.json` with `"@auths/verifier-wasm": "^0.1.0"`. Delete the `prepublishOnly` hook that references the parent repo. 211 212 **Task 7.3** — Fix filename mismatch: `vite.config.ts:44` emits `auths-verify` (no extension), `package.json` expects `auths-verify.mjs`. Change Vite `fileName` to include `.mjs`. 213 214 **Task 7.4** — Fix WASM type mismatch: `verifier-bridge.ts:16-20` and `wasm.d.ts:9-10` declare WASM functions as returning `string` but they return `Promise<string>`. Fix the types and add `await` at call sites (lines 69, 92). 215 216 --- 217 218 ## Epic 8 — GitHub Action Cleanup 219 220 The action works but has several bugs and quality gaps. 221 222 **Task 8.1** — Pin `auths-version` default to `'0.1.0'` in `action.yml` (currently `''`). Resolve `github-token` in `main.ts` with `core.getInput('github-token') || process.env.GITHUB_TOKEN` since `action.yml` defaults don't evaluate expressions. 223 224 **Task 8.2** — Add PR comment deduplication. Currently `main.ts:132-144` creates a new comment on every run. Search for existing comments with a marker and update instead of creating. 225 226 ```typescript 227 const COMMENT_MARKER = '<!-- auths-verify -->'; 228 229 async function upsertPrComment(octokit, prNumber: number, body: string) { 230 const { data: comments } = await octokit.rest.issues.listComments({ 231 ...github.context.repo, 232 issue_number: prNumber, 233 }); 234 const existing = comments.find(c => c.body?.includes(COMMENT_MARKER)); 235 const markedBody = `${COMMENT_MARKER}\n${body}`; 236 if (existing) { 237 await octokit.rest.issues.updateComment({ ...github.context.repo, comment_id: existing.id, body: markedBody }); 238 } else { 239 await octokit.rest.issues.createComment({ ...github.context.repo, issue_number: prNumber, body: markedBody }); 240 } 241 } 242 ``` 243 244 **Task 8.3** — Fix license mismatch: `package.json` says MIT, `LICENSE` file is Apache-2.0. Pick one, update both. 245 246 **Task 8.4** — Add tests for `classifyError()` in `src/verifier.ts:28-37`. Zero test coverage on the most fragile function in the action. 247 248 **Task 8.5** — Add README workflow examples for identity-bundle mode, allowed-signers mode, and PR comment mode. 249 250 --- 251 252 ## Epic 9 — Python SDK Types 253 254 Polish the Python-facing types. Low risk, do after the FFI plumbing is stable. 255 256 **Task 9.1** — Change `verify()`'s `at` parameter from `str | None` to `datetime | None`. Convert to RFC 3339 internally. No deprecation needed. 257 258 **Task 9.2** — Fix `AgentIdentityBundle.attestation_json` — it's always `""` for standalone agents (set in FFI `identity.rs:207`). Change to `Optional[str]`, set to `None` instead of empty string. 259 260 --- 261 262 ## Epic 10 — UMD Bundle for CDN 263 264 Ship a `<script>` tag drop-in. Depends on Epic 7 (widget decoupled). 265 266 **Task 10.1** — Add `vite.config.umd.ts` that builds a UMD bundle with inlined WASM. Register `<auths-verify>` custom element on load. 267 268 **Task 10.2** — Add `unpkg` and `jsdelivr` fields to `package.json`, add `./umd` export, add `build:umd` script. 269 270 --- 271 272 ## Epic 11 — Getting Started Guide 273 274 Write last when APIs are stable. 275 276 **Task 11.1** — Create `docs/getting-started.md`: install CLI, create identity, sign commit, verify in Python, verify in browser (widget), verify in CI (GitHub Action). Exact commands and expected output. Under 3 minutes end-to-end.