From 814ef96624b42f324e57efe51c549fa634783912 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 26 Mar 2026 11:30:06 +0000 Subject: [PATCH 1/9] fix(crypt): address AX import and test review Co-Authored-By: Virgil --- auth/auth_test.go | 5 +++-- auth/session_store_sqlite.go | 4 ++-- cmd/testcmd/output_test.go | 10 +++++----- crypt/chachapoly/chachapoly_test.go | 18 +++++++++--------- crypt/lthn/lthn_test.go | 10 +++++++--- crypt/openpgp/service_test.go | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- trust/policy_test.go | 2 +- trust/trust_test.go | 2 +- 10 files changed, 33 insertions(+), 28 deletions(-) diff --git a/auth/auth_test.go b/auth/auth_test.go index 8560892..5777a8e 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -621,7 +621,8 @@ func TestConcurrentSessionCreation_Good(t *testing.T) { } } -// TestSessionTokenUniqueness_Good generates 1000 tokens and verifies no collisions. +// TestSessionTokenUniqueness_Good generates 1000 session tokens and verifies +// no collisions without paying the full login hash-verification cost each time. func TestSessionTokenUniqueness_Good(t *testing.T) { a, _ := newTestAuth() @@ -633,7 +634,7 @@ func TestSessionTokenUniqueness_Good(t *testing.T) { tokens := make(map[string]bool, n) for i := range n { - session, err := a.Login(userID, "pass") + session, err := a.createSession(userID) require.NoError(t, err) require.NotNil(t, session) diff --git a/auth/session_store_sqlite.go b/auth/session_store_sqlite.go index 843ae58..4340da8 100644 --- a/auth/session_store_sqlite.go +++ b/auth/session_store_sqlite.go @@ -6,12 +6,12 @@ import ( "sync" "time" - "forge.lthn.ai/core/go-store" + "dappco.re/go/core/store" ) const sessionGroup = "sessions" -// SQLiteSessionStore is a SessionStore backed by go-store (SQLite KV). +// SQLiteSessionStore is a SessionStore backed by core/store (SQLite KV). // A mutex serialises all operations because SQLite is single-writer. type SQLiteSessionStore struct { mu sync.Mutex diff --git a/cmd/testcmd/output_test.go b/cmd/testcmd/output_test.go index c208ed0..381befd 100644 --- a/cmd/testcmd/output_test.go +++ b/cmd/testcmd/output_test.go @@ -6,19 +6,19 @@ import ( "github.com/stretchr/testify/assert" ) -func TestShortenPackageName(t *testing.T) { +func TestShortenPackageName_Good(t *testing.T) { assert.Equal(t, "pkg/foo", shortenPackageName("dappco.re/go/core/pkg/foo")) - assert.Equal(t, "cli-php", shortenPackageName("forge.lthn.ai/core/cli-php")) + assert.Equal(t, "cli-php", shortenPackageName("example.com/org/cli-php")) assert.Equal(t, "bar", shortenPackageName("github.com/other/bar")) } -func TestFormatCoverageTest(t *testing.T) { +func TestFormatCoverage_Good(t *testing.T) { assert.Contains(t, formatCoverage(85.0), "85.0%") assert.Contains(t, formatCoverage(65.0), "65.0%") assert.Contains(t, formatCoverage(25.0), "25.0%") } -func TestParseTestOutput(t *testing.T) { +func TestParseTestOutput_Good(t *testing.T) { output := `ok dappco.re/go/core/pkg/foo 0.100s coverage: 50.0% of statements FAIL dappco.re/go/core/pkg/bar ? dappco.re/go/core/pkg/baz [no test files] @@ -33,7 +33,7 @@ FAIL dappco.re/go/core/pkg/bar assert.Equal(t, 50.0, results.packages[0].coverage) } -func TestPrintCoverageSummarySafe(t *testing.T) { +func TestPrintCoverageSummary_Good_LongPackageNames(t *testing.T) { // This tests the bug fix for long package names causing negative Repeat count results := testResults{ packages: []packageCoverage{ diff --git a/crypt/chachapoly/chachapoly_test.go b/crypt/chachapoly/chachapoly_test.go index 1123f2c..da5b96c 100644 --- a/crypt/chachapoly/chachapoly_test.go +++ b/crypt/chachapoly/chachapoly_test.go @@ -15,7 +15,7 @@ func (r *mockReader) Read(p []byte) (n int, err error) { return 0, errors.New("read error") } -func TestEncryptDecrypt(t *testing.T) { +func TestEncryptDecrypt_Good(t *testing.T) { key := make([]byte, 32) for i := range key { key[i] = 1 @@ -31,14 +31,14 @@ func TestEncryptDecrypt(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestEncryptInvalidKeySize(t *testing.T) { +func TestEncrypt_Bad_InvalidKeySize(t *testing.T) { key := make([]byte, 16) // Wrong size plaintext := []byte("test") _, err := Encrypt(plaintext, key) assert.Error(t, err) } -func TestDecryptWithWrongKey(t *testing.T) { +func TestDecrypt_Bad_WrongKey(t *testing.T) { key1 := make([]byte, 32) key2 := make([]byte, 32) key2[0] = 1 // Different key @@ -51,7 +51,7 @@ func TestDecryptWithWrongKey(t *testing.T) { assert.Error(t, err) // Should fail authentication } -func TestDecryptTamperedCiphertext(t *testing.T) { +func TestDecrypt_Bad_TamperedCiphertext(t *testing.T) { key := make([]byte, 32) plaintext := []byte("secret") ciphertext, err := Encrypt(plaintext, key) @@ -64,7 +64,7 @@ func TestDecryptTamperedCiphertext(t *testing.T) { assert.Error(t, err) } -func TestEncryptEmptyPlaintext(t *testing.T) { +func TestEncrypt_Good_EmptyPlaintext(t *testing.T) { key := make([]byte, 32) plaintext := []byte("") ciphertext, err := Encrypt(plaintext, key) @@ -76,7 +76,7 @@ func TestEncryptEmptyPlaintext(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestDecryptShortCiphertext(t *testing.T) { +func TestDecrypt_Bad_ShortCiphertext(t *testing.T) { key := make([]byte, 32) shortCiphertext := []byte("short") @@ -85,7 +85,7 @@ func TestDecryptShortCiphertext(t *testing.T) { assert.Contains(t, err.Error(), "too short") } -func TestCiphertextDiffersFromPlaintext(t *testing.T) { +func TestCiphertextDiffersFromPlaintext_Good(t *testing.T) { key := make([]byte, 32) plaintext := []byte("Hello, world!") ciphertext, err := Encrypt(plaintext, key) @@ -93,7 +93,7 @@ func TestCiphertextDiffersFromPlaintext(t *testing.T) { assert.NotEqual(t, plaintext, ciphertext) } -func TestEncryptNonceError(t *testing.T) { +func TestEncrypt_Bad_NonceError(t *testing.T) { key := make([]byte, 32) plaintext := []byte("test") @@ -106,7 +106,7 @@ func TestEncryptNonceError(t *testing.T) { assert.Error(t, err) } -func TestDecryptInvalidKeySize(t *testing.T) { +func TestDecrypt_Bad_InvalidKeySize(t *testing.T) { key := make([]byte, 16) // Wrong size ciphertext := []byte("test") _, err := Decrypt(ciphertext, key) diff --git a/crypt/lthn/lthn_test.go b/crypt/lthn/lthn_test.go index da0d655..428c0d2 100644 --- a/crypt/lthn/lthn_test.go +++ b/crypt/lthn/lthn_test.go @@ -7,14 +7,18 @@ import ( "github.com/stretchr/testify/assert" ) -func TestHash(t *testing.T) { +func TestHash_Good(t *testing.T) { hash := Hash("hello") assert.NotEmpty(t, hash) } -func TestVerify(t *testing.T) { +func TestVerify_Good(t *testing.T) { hash := Hash("hello") assert.True(t, Verify("hello", hash)) +} + +func TestVerify_Bad(t *testing.T) { + hash := Hash("hello") assert.False(t, Verify("world", hash)) } @@ -50,7 +54,7 @@ func TestCreateSalt_Ugly(t *testing.T) { var testKeyMapMu sync.Mutex -func TestSetKeyMap(t *testing.T) { +func TestSetKeyMap_Good(t *testing.T) { testKeyMapMu.Lock() originalKeyMap := GetKeyMap() t.Cleanup(func() { diff --git a/crypt/openpgp/service_test.go b/crypt/openpgp/service_test.go index 17d8a55..09044ff 100644 --- a/crypt/openpgp/service_test.go +++ b/crypt/openpgp/service_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestCreateKeyPair(t *testing.T) { +func TestCreateKeyPair_Good(t *testing.T) { c := framework.New() s := &Service{core: c} @@ -19,7 +19,7 @@ func TestCreateKeyPair(t *testing.T) { assert.Contains(t, privKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----") } -func TestEncryptDecrypt(t *testing.T) { +func TestEncryptDecrypt_Good(t *testing.T) { c := framework.New() s := &Service{core: c} diff --git a/go.mod b/go.mod index b1656ce..ac2c01f 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( dappco.re/go/core/i18n v0.2.0 dappco.re/go/core/io v0.2.0 dappco.re/go/core/log v0.1.0 + dappco.re/go/core/store v0.2.0 forge.lthn.ai/core/cli v0.3.7 - forge.lthn.ai/core/go-store v0.1.10 github.com/ProtonMail/go-crypto v1.4.0 github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.49.0 diff --git a/go.sum b/go.sum index 78359db..e5bd4d3 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4= dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E= dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc= dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= +dappco.re/go/core/store v0.2.0 h1:MH3R9m3mdr5T3lMWi37ryvTrXzF4xLBTYBGyNZF0p3I= +dappco.re/go/core/store v0.2.0/go.mod h1:QQGJiruayjna3nywbf0N2gcO502q/oEkPoSpBpSKbLM= forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg= forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs= forge.lthn.ai/core/go v0.3.2 h1:VB9pW6ggqBhe438cjfE2iSI5Lg+62MmRbaOFglZM+nQ= @@ -16,8 +18,6 @@ forge.lthn.ai/core/go-inference v0.1.7 h1:9Dy6v03jX5ZRH3n5iTzlYyGtucuBIgSe+S7GWv forge.lthn.ai/core/go-inference v0.1.7/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0= forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= -forge.lthn.ai/core/go-store v0.1.10 h1:JLyf8xMR3V6PfBAW1kv6SJeHsYY93LacEBpTFW657qE= -forge.lthn.ai/core/go-store v0.1.10/go.mod h1:VNnHh94TMD3+L+sSgvxn0GHtDKhJR8FD6JiuIuRtjuk= github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ= github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/trust/policy_test.go b/trust/policy_test.go index 2248377..c656e89 100644 --- a/trust/policy_test.go +++ b/trust/policy_test.go @@ -261,7 +261,7 @@ func TestEvaluate_Good_Tier3IgnoresRepoScope(t *testing.T) { // --- Default rate limits --- -func TestDefaultRateLimit(t *testing.T) { +func TestDefaultRateLimit_Good(t *testing.T) { assert.Equal(t, 10, defaultRateLimit(TierUntrusted)) assert.Equal(t, 60, defaultRateLimit(TierVerified)) assert.Equal(t, 0, defaultRateLimit(TierFull)) diff --git a/trust/trust_test.go b/trust/trust_test.go index 69a5369..e323110 100644 --- a/trust/trust_test.go +++ b/trust/trust_test.go @@ -169,7 +169,7 @@ func TestRegistryListSeq_Good(t *testing.T) { // --- Agent --- -func TestAgentTokenExpiry(t *testing.T) { +func TestAgentTokenExpiry_Good(t *testing.T) { agent := Agent{ Name: "Test", Tier: TierVerified, -- 2.45.3 From f15fb2b475dc635086e94cd30edf1ce61f0bdd66 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 26 Mar 2026 11:31:34 +0000 Subject: [PATCH 2/9] docs: finish AX review follow-ups Co-Authored-By: Virgil --- CLAUDE.md | 2 +- cmd/testcmd/cmd_main.go | 7 ++++++- docs/development.md | 15 ++++++++------- docs/index.md | 8 ++++---- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 811c7b8..7acf703 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,7 +48,7 @@ workspace file instead. | `dappco.re/go/core` | `../go` | Framework: `core.Crypt` interface, `io.Medium` | | `dappco.re/go/core/log` | `../go-log` | `coreerr.E()` contextual error wrapping | | `dappco.re/go/core/io` | `../go-io` | `io.Medium` storage abstraction | -| `forge.lthn.ai/core/go-store` | `../go-store` | SQLite KV store (session persistence) | +| `dappco.re/go/core/store` | `../go-store` | SQLite KV store (session persistence) | | `forge.lthn.ai/core/cli` | `../cli` | CLI framework for `cmd/crypt` commands | No C toolchain or CGo required — all crypto uses pure Go implementations. diff --git a/cmd/testcmd/cmd_main.go b/cmd/testcmd/cmd_main.go index ec98b5b..17892a1 100644 --- a/cmd/testcmd/cmd_main.go +++ b/cmd/testcmd/cmd_main.go @@ -20,7 +20,7 @@ var ( testCovLowStyle = cli.NewStyle().Foreground(cli.ColourRed500) ) -// Flag variables for test command +// Flag variables for test command. var ( testVerbose bool testCoverage bool @@ -31,10 +31,15 @@ var ( testJSON bool ) +// testCmd wraps `go test`, defaulting to `./...` and keeping coverage enabled +// so both human-readable and JSON summaries can report package coverage. var testCmd = &cli.Command{ Use: "test", Short: i18n.T("cmd.test.short"), Long: i18n.T("cmd.test.long"), + Example: ` core test + core test --pkg ./auth --run TestLogin_Good + core test --race --json`, RunE: func(cmd *cli.Command, args []string) error { return runTest(testVerbose, testCoverage, testShort, testPkg, testRun, testRace, testJSON) }, diff --git a/docs/development.md b/docs/development.md index 71a2cf9..c7964d9 100644 --- a/docs/development.md +++ b/docs/development.md @@ -9,8 +9,8 @@ description: How to build, test, and contribute to go-crypt. - **Go 1.26** or later (the module declares `go 1.26.0`). - A Go workspace (`go.work`) that resolves the local dependencies: - `forge.lthn.ai/core/go`, `forge.lthn.ai/core/go-store`, - `forge.lthn.ai/core/go-io`, `forge.lthn.ai/core/go-log`, and + `dappco.re/go/core`, `dappco.re/go/core/store`, + `dappco.re/go/core/io`, `dappco.re/go/core/log`, and `forge.lthn.ai/core/cli`. If you are working outside the full monorepo, create a `go.work` at the parent directory pointing to your local checkouts. @@ -211,16 +211,17 @@ HTTPS authentication is not configured for this repository. ## Local Dependencies -The `go.mod` depends on several `forge.lthn.ai/core/*` modules. These are +The `go.mod` depends on several `dappco.re/go/core/*` modules plus the +remaining `forge.lthn.ai/core/cli` dependency. These are resolved through the Go workspace (`~/Code/go.work`). Do not modify the replace directives in `go.mod` directly -- use the workspace file instead. | Module | Local Path | Purpose | |--------|-----------|---------| -| `forge.lthn.ai/core/go` | `../go` | Framework: `core.Crypt` interface, `io.Medium` | -| `forge.lthn.ai/core/go-store` | `../go-store` | SQLite KV store for session persistence | -| `forge.lthn.ai/core/go-io` | `../go-io` | `io.Medium` storage abstraction | -| `forge.lthn.ai/core/go-log` | `../go-log` | `core.E()` contextual error wrapping | +| `dappco.re/go/core` | `../go` | Framework: `core.Crypt` interface, `io.Medium` | +| `dappco.re/go/core/store` | `../go-store` | SQLite KV store for session persistence | +| `dappco.re/go/core/io` | `../go-io` | `io.Medium` storage abstraction | +| `dappco.re/go/core/log` | `../go-log` | `core.E()` contextual error wrapping | | `forge.lthn.ai/core/cli` | `../cli` | CLI framework for `cmd/crypt` commands | ## Known Limitations diff --git a/docs/index.md b/docs/index.md index 3916142..49db429 100644 --- a/docs/index.md +++ b/docs/index.md @@ -144,10 +144,10 @@ core crypt checksum myfile.txt --verify "abc123..." | Module | Role | |--------|------| -| `forge.lthn.ai/core/go` | Framework: `core.E` error helper, `core.Crypt` interface, `io.Medium` storage abstraction | -| `forge.lthn.ai/core/go-store` | SQLite KV store for persistent session storage | -| `forge.lthn.ai/core/go-io` | `io.Medium` interface used by the auth package | -| `forge.lthn.ai/core/go-log` | Contextual error wrapping via `core.E()` | +| `dappco.re/go/core` | Framework: `core.E` error helper, `core.Crypt` interface, `io.Medium` storage abstraction | +| `dappco.re/go/core/store` | SQLite KV store for persistent session storage | +| `dappco.re/go/core/io` | `io.Medium` interface used by the auth package | +| `dappco.re/go/core/log` | Contextual error wrapping via `core.E()` | | `forge.lthn.ai/core/cli` | CLI framework for the `cmd/crypt` commands | | `github.com/ProtonMail/go-crypto` | OpenPGP implementation (actively maintained, post-quantum research) | | `golang.org/x/crypto` | Argon2id, ChaCha20-Poly1305, scrypt, HKDF, bcrypt | -- 2.45.3 From 87f87bfc0a5c63baf7df94082653737275fd8ffe Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 26 Mar 2026 13:06:25 +0000 Subject: [PATCH 3/9] docs(crypt): add ax v0.8.0 upgrade plan Co-Authored-By: Virgil --- UPGRADE.md | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 UPGRADE.md diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..d360446 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,149 @@ +# Upgrade Plan: v0.8.0 AX Compliance + +Scope: planning only. This document identifies the repo changes required for AX v0.8.0 compliance without changing package code yet. + +Breaking-change risk: medium. This module is consumed by `core`, `go-blockchain`, and `LEM`, so any public API edits made during compliance work should be reviewed as a consumer-facing change even if the primary AX items are mechanical. + +## Current Audit Summary + +- Banned imports: 52 hits across production and test code. +- Test naming violations: 292 `Test...` functions do not follow `TestFile_Function_{Good,Bad,Ugly}`. +- Exported API usage-example gaps: 162 exported declarations in non-test files do not include a usage/example-oriented doc comment. + +## Execution Order + +1. Fix production banned imports first to reduce AX failures without changing public behaviour. +2. Rename tests package-by-package, starting with the largest files, and rerun targeted `go test ./...`. +3. Add usage/example comments to exported API surfaces, starting with the packages most likely to be imported by consumers: `auth`, `crypt`, `trust`. +4. Run the full validation flow: `go build ./...`, `go vet ./...`, `go test ./... -count=1 -timeout 120s`, `go test -cover ./...`, `go mod tidy`. +5. Review downstream consumers for naming-only vs behavioural risk before releasing v0.8.0. + +## 1. Remove Banned Imports + +Production code should be prioritised before test helpers. + +### Production files + +- `auth/auth.go`: replace banned imports at `auth/auth.go:32`, `auth/auth.go:33`, `auth/auth.go:34`. +- `auth/session_store_sqlite.go`: replace banned imports at `auth/session_store_sqlite.go:4`, `auth/session_store_sqlite.go:5`. +- `cmd/crypt/cmd_checksum.go`: replace banned imports at `cmd/crypt/cmd_checksum.go:4`, `cmd/crypt/cmd_checksum.go:5`. +- `cmd/crypt/cmd_encrypt.go`: replace banned imports at `cmd/crypt/cmd_encrypt.go:4`, `cmd/crypt/cmd_encrypt.go:5`. +- `cmd/crypt/cmd_hash.go`: replace banned import at `cmd/crypt/cmd_hash.go:4`. +- `cmd/crypt/cmd_keygen.go`: replace banned import at `cmd/crypt/cmd_keygen.go:7`. +- `cmd/testcmd/cmd_output.go`: replace banned imports at `cmd/testcmd/cmd_output.go:6`, `cmd/testcmd/cmd_output.go:7`, `cmd/testcmd/cmd_output.go:11`. +- `cmd/testcmd/cmd_runner.go`: replace banned imports at `cmd/testcmd/cmd_runner.go:5`, `cmd/testcmd/cmd_runner.go:7`, `cmd/testcmd/cmd_runner.go:8`, `cmd/testcmd/cmd_runner.go:10`. +- `crypt/chachapoly/chachapoly.go`: replace banned import at `crypt/chachapoly/chachapoly.go:5`. +- `crypt/checksum.go`: replace banned import at `crypt/checksum.go:8`. +- `crypt/hash.go`: replace banned imports at `crypt/hash.go:6`, `crypt/hash.go:7`. +- `crypt/openpgp/service.go`: replace banned import at `crypt/openpgp/service.go:7`. +- `crypt/rsa/rsa.go`: replace banned import at `crypt/rsa/rsa.go:9`. +- `trust/approval.go`: replace banned import at `trust/approval.go:4`. +- `trust/audit.go`: replace banned import at `trust/audit.go:4`. +- `trust/config.go`: replace banned imports at `trust/config.go:4`, `trust/config.go:5`, `trust/config.go:7`. +- `trust/policy.go`: replace banned imports at `trust/policy.go:4`, `trust/policy.go:6`. +- `trust/trust.go`: replace banned import at `trust/trust.go:14`. + +### Test files + +- `auth/auth_test.go`: banned imports at `auth/auth_test.go:4`, `auth/auth_test.go:5`, `auth/auth_test.go:6`. +- `auth/session_store_test.go`: banned imports at `auth/session_store_test.go:5`, `auth/session_store_test.go:6`, `auth/session_store_test.go:7`. +- `crypt/chachapoly/chachapoly_test.go`: banned import at `crypt/chachapoly/chachapoly_test.go:5`. +- `crypt/checksum_test.go`: banned imports at `crypt/checksum_test.go:4`, `crypt/checksum_test.go:5`. +- `crypt/rsa/rsa_test.go`: banned import at `crypt/rsa/rsa_test.go:9`. +- `trust/approval_test.go`: banned import at `trust/approval_test.go:4`. +- `trust/audit_test.go`: banned imports at `trust/audit_test.go:5`, `trust/audit_test.go:6`, `trust/audit_test.go:8`. +- `trust/bench_test.go`: banned import at `trust/bench_test.go:4`. +- `trust/config_test.go`: banned imports at `trust/config_test.go:5`, `trust/config_test.go:6`, `trust/config_test.go:7`, `trust/config_test.go:8`. +- `trust/trust_test.go`: banned import at `trust/trust_test.go:4`. + +Plan notes: + +- Prefer package-local replacements already used elsewhere in the repo rather than introducing new compatibility shims. +- `cmd/testcmd/cmd_runner.go:8` is the only `os/exec` hit and may need the largest redesign if AX forbids subprocess execution outright. +- `encoding/json` use is concentrated in `auth` and `trust`; validate AX-approved serialisation alternatives before changing wire formats consumed by downstream modules. + +## 2. Rename Tests To `TestFile_Function_{Good,Bad,Ugly}` + +Every audited `Test...` function currently fails the file-prefix requirement. The work is mechanical but broad, so do it file-by-file to keep diffs reviewable. + +### Highest-volume files first + +- `auth/auth_test.go`: 55 renames, starting at `auth/auth_test.go:28`. +- `trust/policy_test.go`: 38 renames, starting at `trust/policy_test.go:32`. +- `trust/trust_test.go`: 26 renames, starting at `trust/trust_test.go:15`. +- `trust/approval_test.go`: 23 renames, starting at `trust/approval_test.go:14`. +- `trust/scope_test.go`: 23 renames, starting at `trust/scope_test.go:12`. +- `trust/config_test.go`: 20 renames, starting at `trust/config_test.go:37`. +- `auth/session_store_test.go`: 19 renames, starting at `auth/session_store_test.go:21`. +- `trust/audit_test.go`: 17 renames, starting at `trust/audit_test.go:18`. + +### Remaining files + +- `crypt/pgp/pgp_test.go`: 12 renames, starting at `crypt/pgp/pgp_test.go:10`. +- `crypt/chachapoly/chachapoly_test.go`: 9 renames, starting at `crypt/chachapoly/chachapoly_test.go:18`. +- `crypt/symmetric_test.go`: 8 renames, starting at `crypt/symmetric_test.go:10`. +- `crypt/checksum_test.go`: 7 renames, starting at `crypt/checksum_test.go:12`. +- `crypt/crypt_test.go`: 7 renames, starting at `crypt/crypt_test.go:11`. +- `crypt/lthn/lthn_test.go`: 7 renames, starting at `crypt/lthn/lthn_test.go:10`. +- `crypt/kdf_test.go`: 6 renames, starting at `crypt/kdf_test.go:9`. +- `cmd/testcmd/output_test.go`: 4 renames, starting at `cmd/testcmd/output_test.go:9`. +- `crypt/hash_test.go`: 3 renames, starting at `crypt/hash_test.go:10`. +- `crypt/hmac_test.go`: 3 renames, starting at `crypt/hmac_test.go:11`. +- `crypt/rsa/rsa_test.go`: 3 renames, starting at `crypt/rsa/rsa_test.go:22`. +- `crypt/openpgp/service_test.go`: 2 renames, starting at `crypt/openpgp/service_test.go:12`. + +Plan notes: + +- Apply the file stem as the prefix. Examples: `approval_test.go` -> `TestApproval_...`, `policy_test.go` -> `TestPolicy_...`, `session_store_test.go` -> `TestSessionStore_...`. +- Preserve existing subcase suffixes after `Good`, `Bad`, or `Ugly` where they add meaning. +- Renames should not change behaviour, but they can break editor tooling, test filters, and CI allowlists that depend on old names. + +## 3. Add Usage-Example Comments To Exported API + +The package has normal doc comments in many places, but the audit found no usage/example-oriented export comments matching AX expectations. This affects all major public surfaces. + +### `auth` package + +- `auth/auth.go`: 26 exported declarations at `auth/auth.go:47`, `auth/auth.go:48`, `auth/auth.go:60`, `auth/auth.go:70`, `auth/auth.go:77`, `auth/auth.go:85`, `auth/auth.go:92`, `auth/auth.go:95`, `auth/auth.go:102`, `auth/auth.go:110`, `auth/auth.go:125`, `auth/auth.go:138`, `auth/auth.go:164`, `auth/auth.go:241`, `auth/auth.go:283`, `auth/auth.go:317`, `auth/auth.go:334`, `auth/auth.go:355`, `auth/auth.go:367`, `auth/auth.go:407`, `auth/auth.go:459`, `auth/auth.go:540`, `auth/auth.go:576`, `auth/auth.go:600`, `auth/auth.go:623`, `auth/auth.go:696`. +- `auth/hardware.go`: 2 exported declarations at `auth/hardware.go:20`, `auth/hardware.go:47`. +- `auth/session_store.go`: 9 exported declarations at `auth/session_store.go:12`, `auth/session_store.go:15`, `auth/session_store.go:24`, `auth/session_store.go:30`, `auth/session_store.go:37`, `auth/session_store.go:52`, `auth/session_store.go:63`, `auth/session_store.go:76`, `auth/session_store.go:87`. +- `auth/session_store_sqlite.go`: 8 exported declarations at `auth/session_store_sqlite.go:16`, `auth/session_store_sqlite.go:23`, `auth/session_store_sqlite.go:32`, `auth/session_store_sqlite.go:52`, `auth/session_store_sqlite.go:64`, `auth/session_store_sqlite.go:80`, `auth/session_store_sqlite.go:104`, `auth/session_store_sqlite.go:131`. + +### `crypt` package family + +- `crypt/chachapoly/chachapoly.go`: 2 exported declarations at `crypt/chachapoly/chachapoly.go:14`, `crypt/chachapoly/chachapoly.go:29`. +- `crypt/checksum.go`: 4 exported declarations at `crypt/checksum.go:14`, `crypt/checksum.go:30`, `crypt/checksum.go:46`, `crypt/checksum.go:52`. +- `crypt/crypt.go`: 4 exported declarations at `crypt/crypt.go:10`, `crypt/crypt.go:32`, `crypt/crypt.go:53`, `crypt/crypt.go:74`. +- `crypt/hash.go`: 4 exported declarations at `crypt/hash.go:17`, `crypt/hash.go:37`, `crypt/hash.go:72`, `crypt/hash.go:81`. +- `crypt/hmac.go`: 3 exported declarations at `crypt/hmac.go:11`, `crypt/hmac.go:18`, `crypt/hmac.go:26`. +- `crypt/kdf.go`: 3 exported declarations at `crypt/kdf.go:28`, `crypt/kdf.go:34`, `crypt/kdf.go:45`. +- `crypt/lthn/lthn.go`: 4 exported declarations at `crypt/lthn/lthn.go:45`, `crypt/lthn/lthn.go:50`, `crypt/lthn/lthn.go:64`, `crypt/lthn/lthn.go:92`. +- `crypt/openpgp/service.go`: 6 exported declarations at `crypt/openpgp/service.go:18`, `crypt/openpgp/service.go:23`, `crypt/openpgp/service.go:29`, `crypt/openpgp/service.go:104`, `crypt/openpgp/service.go:139`, `crypt/openpgp/service.go:177`. +- `crypt/pgp/pgp.go`: 6 exported declarations at `crypt/pgp/pgp.go:19`, `crypt/pgp/pgp.go:27`, `crypt/pgp/pgp.go:119`, `crypt/pgp/pgp.go:152`, `crypt/pgp/pgp.go:196`, `crypt/pgp/pgp.go:227`. +- `crypt/rsa/rsa.go`: 5 exported declarations at `crypt/rsa/rsa.go:15`, `crypt/rsa/rsa.go:18`, `crypt/rsa/rsa.go:23`, `crypt/rsa/rsa.go:53`, `crypt/rsa/rsa.go:80`. +- `crypt/symmetric.go`: 4 exported declarations at `crypt/symmetric.go:16`, `crypt/symmetric.go:33`, `crypt/symmetric.go:56`, `crypt/symmetric.go:78`. + +### `trust` package + +- `trust/approval.go`: 15 exported declarations at `trust/approval.go:13`, `trust/approval.go:17`, `trust/approval.go:19`, `trust/approval.go:21`, `trust/approval.go:25`, `trust/approval.go:39`, `trust/approval.go:61`, `trust/approval.go:68`, `trust/approval.go:76`, `trust/approval.go:104`, `trust/approval.go:125`, `trust/approval.go:145`, `trust/approval.go:159`, `trust/approval.go:173`, `trust/approval.go:189`. +- `trust/audit.go`: 11 exported declarations at `trust/audit.go:14`, `trust/audit.go:30`, `trust/audit.go:35`, `trust/audit.go:54`, `trust/audit.go:62`, `trust/audit.go:69`, `trust/audit.go:99`, `trust/audit.go:109`, `trust/audit.go:123`, `trust/audit.go:130`, `trust/audit.go:144`. +- `trust/config.go`: 7 exported declarations at `trust/config.go:13`, `trust/config.go:21`, `trust/config.go:26`, `trust/config.go:36`, `trust/config.go:70`, `trust/config.go:84`, `trust/config.go:94`. +- `trust/policy.go`: 12 exported declarations at `trust/policy.go:12`, `trust/policy.go:24`, `trust/policy.go:30`, `trust/policy.go:34`, `trust/policy.go:36`, `trust/policy.go:38`, `trust/policy.go:42`, `trust/policy.go:56`, `trust/policy.go:64`, `trust/policy.go:76`, `trust/policy.go:149`, `trust/policy.go:158`. +- `trust/trust.go`: 25 exported declarations at `trust/trust.go:23`, `trust/trust.go:27`, `trust/trust.go:29`, `trust/trust.go:31`, `trust/trust.go:35`, `trust/trust.go:49`, `trust/trust.go:54`, `trust/trust.go:57`, `trust/trust.go:58`, `trust/trust.go:59`, `trust/trust.go:60`, `trust/trust.go:61`, `trust/trust.go:62`, `trust/trust.go:63`, `trust/trust.go:64`, `trust/trust.go:65`, `trust/trust.go:69`, `trust/trust.go:86`, `trust/trust.go:92`, `trust/trust.go:100`, `trust/trust.go:121`, `trust/trust.go:128`, `trust/trust.go:139`, `trust/trust.go:150`, `trust/trust.go:163`. + +### Command entrypoints + +- `cmd/crypt/cmd.go`: exported declaration at `cmd/crypt/cmd.go:10`. +- `cmd/testcmd/cmd_main.go`: exported declaration at `cmd/testcmd/cmd_main.go:59`. + +Plan notes: + +- Keep comments example-oriented and consumer-safe. Public comments should explain the happy-path call pattern without embedding secrets or unstable implementation details. +- Prioritise constructor and entrypoint comments first: `auth.New`, `trust.NewRegistry`, `trust.NewPolicyEngine`, `trust.NewApprovalQueue`, `trust.NewAuditLog`, `crypt/openpgp.New`, `crypt/rsa.NewService`, and the command registration functions. +- If AX requires literal `Usage:` blocks, standardise that wording repo-wide before editing all 162 declarations. + +## 4. Validation And Release Checks + +- Run the required flow after each migration stage, not only at the end, so failures stay attributable to one category of change. +- Add a final review for consumer impact before tagging `v0.8.0`, especially if banned-import replacements force API or behaviour adjustments in `auth`, `trust/config`, or CLI codepaths. +- Commit the compliance work in small conventional-commit slices where practical. If the migration is done in one batch, use a non-breaking conventional commit scope that matches the touched package set. -- 2.45.3 From f46cd04e2fb25805ef8299c63b79027c80e67bcd Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 26 Mar 2026 15:21:24 +0000 Subject: [PATCH 4/9] refactor: upgrade core v0.8.0-alpha.1 and replace banned imports Co-Authored-By: Virgil --- auth/auth.go | 47 ++++++------ auth/auth_test.go | 38 +++++----- auth/session_store_sqlite.go | 24 ++++--- auth/session_store_test.go | 27 ++++--- cmd/crypt/cmd_checksum.go | 14 ++-- cmd/crypt/cmd_encrypt.go | 10 ++- cmd/crypt/cmd_hash.go | 7 +- cmd/crypt/cmd_keygen.go | 8 +-- cmd/testcmd/cmd_output.go | 108 ++++++++++++++++------------ cmd/testcmd/cmd_runner.go | 91 ++++++++++++----------- crypt/chachapoly/chachapoly.go | 4 +- crypt/chachapoly/chachapoly_test.go | 4 +- crypt/checksum.go | 14 ++-- crypt/checksum_test.go | 21 +++--- crypt/hash.go | 67 ++++++++++++++--- crypt/openpgp/service.go | 7 +- crypt/rsa/rsa.go | 4 +- crypt/rsa/rsa_test.go | 4 +- go.mod | 4 +- go.sum | 6 +- trust/approval.go | 14 ++-- trust/approval_test.go | 4 +- trust/audit.go | 20 ++++-- trust/audit_test.go | 55 +++++++------- trust/bench_test.go | 13 ++-- trust/config.go | 97 ++++++++++++++++++++----- trust/config_test.go | 59 +++++++-------- trust/policy.go | 35 +++++---- trust/trust.go | 6 +- trust/trust_test.go | 12 ++-- 30 files changed, 485 insertions(+), 339 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index 35db896..cf7417e 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -29,12 +29,10 @@ import ( "context" "crypto/rand" "encoding/hex" - "encoding/json" - "fmt" - "strings" "sync" "time" + core "dappco.re/go/core" "dappco.re/go/core/crypt/crypt" "dappco.re/go/core/crypt/crypt/lthn" "dappco.re/go/core/crypt/crypt/pgp" @@ -218,12 +216,13 @@ func (a *Authenticator) Register(username, password string) (*User, error) { } // Encrypt metadata with the user's public key and store - metaJSON, err := json.Marshal(user) - if err != nil { + metaJSONResult := core.JSONMarshal(user) + if !metaJSONResult.OK { + err, _ := metaJSONResult.Value.(error) return nil, coreerr.E(op, "failed to marshal user metadata", err) } - encMeta, err := pgp.Encrypt(metaJSON, kp.PublicKey) + encMeta, err := pgp.Encrypt(metaJSONResult.Value.([]byte), kp.PublicKey) if err != nil { return nil, coreerr.E(op, "failed to encrypt user metadata", err) } @@ -419,7 +418,7 @@ func (a *Authenticator) Login(userID, password string) (*Session, error) { return nil, coreerr.E(op, "failed to read password hash", err) } - if strings.HasPrefix(storedHash, "$argon2id$") { + if core.HasPrefix(storedHash, "$argon2id$") { valid, err := crypt.VerifyPassword(password, storedHash) if err != nil { return nil, coreerr.E(op, "failed to verify password", err) @@ -482,7 +481,9 @@ func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) ( } var user User - if err := json.Unmarshal(metaJSON, &user); err != nil { + metaResult := core.JSONUnmarshal(metaJSON, &user) + if !metaResult.OK { + err, _ := metaResult.Value.(error) return nil, coreerr.E(op, "failed to unmarshal user metadata", err) } @@ -504,12 +505,13 @@ func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) ( user.PasswordHash = newHash // Re-encrypt metadata with new public key - updatedMeta, err := json.Marshal(&user) - if err != nil { + updatedMetaResult := core.JSONMarshal(&user) + if !updatedMetaResult.OK { + err, _ := updatedMetaResult.Value.(error) return nil, coreerr.E(op, "failed to marshal updated metadata", err) } - encUpdatedMeta, err := pgp.Encrypt(updatedMeta, newKP.PublicKey) + encUpdatedMeta, err := pgp.Encrypt(updatedMetaResult.Value.([]byte), newKP.PublicKey) if err != nil { return nil, coreerr.E(op, "failed to encrypt metadata with new key", err) } @@ -556,11 +558,12 @@ func (a *Authenticator) RevokeKey(userID, password, reason string) error { Reason: reason, RevokedAt: time.Now(), } - revJSON, err := json.Marshal(&rev) - if err != nil { + revJSONResult := core.JSONMarshal(&rev) + if !revJSONResult.OK { + err, _ := revJSONResult.Value.(error) return coreerr.E(op, "failed to marshal revocation record", err) } - if err := a.medium.Write(userPath(userID, ".rev"), string(revJSON)); err != nil { + if err := a.medium.Write(userPath(userID, ".rev"), string(revJSONResult.Value.([]byte))); err != nil { return coreerr.E(op, "failed to write revocation record", err) } @@ -586,7 +589,8 @@ func (a *Authenticator) IsRevoked(userID string) bool { // Attempt to parse as JSON revocation record var rev Revocation - if err := json.Unmarshal([]byte(content), &rev); err != nil { + revResult := core.JSONUnmarshal([]byte(content), &rev) + if !revResult.OK { return false } @@ -605,12 +609,13 @@ func (a *Authenticator) WriteChallengeFile(userID, path string) error { return coreerr.E(op, "failed to create challenge", err) } - data, err := json.Marshal(challenge) - if err != nil { + challengeResult := core.JSONMarshal(challenge) + if !challengeResult.OK { + err, _ := challengeResult.Value.(error) return coreerr.E(op, "failed to marshal challenge", err) } - if err := a.medium.Write(path, string(data)); err != nil { + if err := a.medium.Write(path, string(challengeResult.Value.([]byte))); err != nil { return coreerr.E(op, "failed to write challenge file", err) } @@ -645,7 +650,7 @@ func (a *Authenticator) verifyPassword(userID, password string) error { // Try Argon2id hash first (.hash file) if a.medium.IsFile(userPath(userID, ".hash")) { storedHash, err := a.medium.Read(userPath(userID, ".hash")) - if err == nil && strings.HasPrefix(storedHash, "$argon2id$") { + if err == nil && core.HasPrefix(storedHash, "$argon2id$") { valid, verr := crypt.VerifyPassword(password, storedHash) if verr != nil { return coreerr.E(op, "failed to verify password", nil) @@ -705,11 +710,11 @@ func (a *Authenticator) StartCleanup(ctx context.Context, interval time.Duration case <-ticker.C: count, err := a.store.Cleanup() if err != nil { - fmt.Printf("auth: session cleanup error: %v\n", err) + core.Print(nil, "auth: session cleanup error: %v", err) continue } if count > 0 { - fmt.Printf("auth: cleaned up %d expired session(s)\n", count) + core.Print(nil, "auth: cleaned up %d expired session(s)", count) } } } diff --git a/auth/auth_test.go b/auth/auth_test.go index 5777a8e..97a3c17 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -1,13 +1,11 @@ package auth import ( - "encoding/json" - "fmt" - "strings" "sync" "testing" "time" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -46,7 +44,7 @@ func TestRegister_Good(t *testing.T) { assert.NotEmpty(t, user.PublicKey) assert.Equal(t, userID, user.KeyID) assert.NotEmpty(t, user.Fingerprint) - assert.True(t, strings.HasPrefix(user.PasswordHash, "$argon2id$"), "password hash should be Argon2id format") + assert.True(t, core.HasPrefix(user.PasswordHash, "$argon2id$"), "password hash should be Argon2id format") assert.False(t, user.Created.IsZero()) } @@ -414,8 +412,8 @@ func TestAirGappedFlow_Good(t *testing.T) { require.NoError(t, err) var challenge Challenge - err = json.Unmarshal([]byte(challengeData), &challenge) - require.NoError(t, err) + result := core.JSONUnmarshal([]byte(challengeData), &challenge) + require.Truef(t, result.OK, "failed to unmarshal challenge: %v", result.Value) // Client-side: decrypt nonce and sign it privKey, err := m.Read(userPath(userID, ".key")) @@ -590,7 +588,7 @@ func TestConcurrentSessionCreation_Good(t *testing.T) { const n = 10 userIDs := make([]string, n) for i := range n { - username := fmt.Sprintf("concurrent-user-%d", i) + username := core.Sprintf("concurrent-user-%d", i) _, err := a.Register(username, "pass") require.NoError(t, err) userIDs[i] = lthn.Hash(username) @@ -736,7 +734,11 @@ func TestEmptyPasswordRegistration_Good(t *testing.T) { func TestVeryLongUsername_Ugly(t *testing.T) { a, _ := newTestAuth() - longUsername := strings.Repeat("a", 10000) + longName := core.NewBuilder() + for range 10000 { + longName.WriteString("a") + } + longUsername := longName.String() user, err := a.Register(longUsername, "pass") require.NoError(t, err) require.NotNil(t, user) @@ -795,8 +797,8 @@ func TestAirGappedRoundTrip_Good(t *testing.T) { require.NoError(t, err) var challenge Challenge - err = json.Unmarshal([]byte(challengeData), &challenge) - require.NoError(t, err) + result := core.JSONUnmarshal([]byte(challengeData), &challenge) + require.Truef(t, result.OK, "failed to unmarshal challenge: %v", result.Value) assert.NotEmpty(t, challenge.Encrypted) assert.True(t, challenge.ExpiresAt.After(time.Now())) @@ -870,13 +872,13 @@ func TestRegisterArgon2id_Good(t *testing.T) { assert.True(t, m.IsFile(userPath(userID, ".hash"))) hashContent, err := m.Read(userPath(userID, ".hash")) require.NoError(t, err) - assert.True(t, strings.HasPrefix(hashContent, "$argon2id$"), "stored hash should be Argon2id") + assert.True(t, core.HasPrefix(hashContent, "$argon2id$"), "stored hash should be Argon2id") // .lthn file should NOT exist for new registrations assert.False(t, m.IsFile(userPath(userID, ".lthn"))) // User struct should have Argon2id hash - assert.True(t, strings.HasPrefix(user.PasswordHash, "$argon2id$")) + assert.True(t, core.HasPrefix(user.PasswordHash, "$argon2id$")) } // TestLoginArgon2id_Good verifies login works with Argon2id hashed password. @@ -940,7 +942,7 @@ func TestLegacyLTHNMigration_Good(t *testing.T) { assert.True(t, m.IsFile(userPath(userID, ".hash")), "migration should create .hash file") newHash, err := m.Read(userPath(userID, ".hash")) require.NoError(t, err) - assert.True(t, strings.HasPrefix(newHash, "$argon2id$"), "migrated hash should be Argon2id") + assert.True(t, core.HasPrefix(newHash, "$argon2id$"), "migrated hash should be Argon2id") // Subsequent login should use the new Argon2id hash (not LTHN) session2, err := a.Login(userID, "legacy-pass") @@ -1024,10 +1026,10 @@ func TestRotateKeyPair_Good(t *testing.T) { require.NoError(t, err) var meta User - err = json.Unmarshal(decrypted, &meta) - require.NoError(t, err) + result := core.JSONUnmarshal(decrypted, &meta) + require.Truef(t, result.OK, "failed to unmarshal metadata: %v", result.Value) assert.Equal(t, userID, meta.KeyID) - assert.True(t, strings.HasPrefix(meta.PasswordHash, "$argon2id$")) + assert.True(t, core.HasPrefix(meta.PasswordHash, "$argon2id$")) } // TestRotateKeyPair_Bad verifies that rotation fails with wrong old password. @@ -1108,8 +1110,8 @@ func TestRevokeKey_Good(t *testing.T) { assert.NotEqual(t, "REVOCATION_PLACEHOLDER", revContent) var rev Revocation - err = json.Unmarshal([]byte(revContent), &rev) - require.NoError(t, err) + result := core.JSONUnmarshal([]byte(revContent), &rev) + require.Truef(t, result.OK, "failed to unmarshal revocation: %v", result.Value) assert.Equal(t, userID, rev.UserID) assert.Equal(t, "compromised key material", rev.Reason) assert.False(t, rev.RevokedAt.IsZero()) diff --git a/auth/session_store_sqlite.go b/auth/session_store_sqlite.go index 4340da8..1d61ff2 100644 --- a/auth/session_store_sqlite.go +++ b/auth/session_store_sqlite.go @@ -1,11 +1,10 @@ package auth import ( - "encoding/json" - "errors" "sync" "time" + core "dappco.re/go/core" "dappco.re/go/core/store" ) @@ -35,14 +34,16 @@ func (s *SQLiteSessionStore) Get(token string) (*Session, error) { val, err := s.store.Get(sessionGroup, token) if err != nil { - if errors.Is(err, store.ErrNotFound) { + if core.Is(err, store.ErrNotFound) { return nil, ErrSessionNotFound } return nil, err } var session Session - if err := json.Unmarshal([]byte(val), &session); err != nil { + result := core.JSONUnmarshal([]byte(val), &session) + if !result.OK { + err, _ := result.Value.(error) return nil, err } return &session, nil @@ -53,11 +54,12 @@ func (s *SQLiteSessionStore) Set(session *Session) error { s.mu.Lock() defer s.mu.Unlock() - data, err := json.Marshal(session) - if err != nil { + result := core.JSONMarshal(session) + if !result.OK { + err, _ := result.Value.(error) return err } - return s.store.Set(sessionGroup, session.Token, string(data)) + return s.store.Set(sessionGroup, session.Token, string(result.Value.([]byte))) } // Delete removes a session by token from SQLite. @@ -68,7 +70,7 @@ func (s *SQLiteSessionStore) Delete(token string) error { // Check existence first to return ErrSessionNotFound _, err := s.store.Get(sessionGroup, token) if err != nil { - if errors.Is(err, store.ErrNotFound) { + if core.Is(err, store.ErrNotFound) { return ErrSessionNotFound } return err @@ -88,7 +90,8 @@ func (s *SQLiteSessionStore) DeleteByUser(userID string) error { for token, val := range all { var session Session - if err := json.Unmarshal([]byte(val), &session); err != nil { + result := core.JSONUnmarshal([]byte(val), &session) + if !result.OK { continue // Skip malformed entries } if session.UserID == userID { @@ -114,7 +117,8 @@ func (s *SQLiteSessionStore) Cleanup() (int, error) { count := 0 for token, val := range all { var session Session - if err := json.Unmarshal([]byte(val), &session); err != nil { + result := core.JSONUnmarshal([]byte(val), &session) + if !result.OK { continue // Skip malformed entries } if now.After(session.ExpiresAt) { diff --git a/auth/session_store_test.go b/auth/session_store_test.go index 69fdbfa..977e159 100644 --- a/auth/session_store_test.go +++ b/auth/session_store_test.go @@ -2,13 +2,11 @@ package auth import ( "context" - "fmt" - "os" - "path/filepath" "sync" "testing" "time" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -66,7 +64,7 @@ func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) { // Create sessions for two users for i := range 3 { err := store.Set(&Session{ - Token: fmt.Sprintf("user-a-token-%d", i), + Token: core.Sprintf("user-a-token-%d", i), UserID: "user-a", ExpiresAt: time.Now().Add(1 * time.Hour), }) @@ -86,7 +84,7 @@ func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) { // user-a sessions should be gone for i := range 3 { - _, err := store.Get(fmt.Sprintf("user-a-token-%d", i)) + _, err := store.Get(core.Sprintf("user-a-token-%d", i)) assert.ErrorIs(t, err, ErrSessionNotFound) } @@ -146,11 +144,11 @@ func TestMemorySessionStore_Concurrent_Good(t *testing.T) { for i := range n { go func(idx int) { defer wg.Done() - token := fmt.Sprintf("concurrent-token-%d", idx) + token := core.Sprintf("concurrent-token-%d", idx) err := store.Set(&Session{ Token: token, - UserID: fmt.Sprintf("user-%d", idx%5), + UserID: core.Sprintf("user-%d", idx%5), ExpiresAt: time.Now().Add(1 * time.Hour), }) assert.NoError(t, err) @@ -222,7 +220,7 @@ func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) { // Create sessions for two users for i := range 3 { err := store.Set(&Session{ - Token: fmt.Sprintf("sqlite-user-a-%d", i), + Token: core.Sprintf("sqlite-user-a-%d", i), UserID: "user-a", ExpiresAt: time.Now().Add(1 * time.Hour), }) @@ -242,7 +240,7 @@ func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) { // user-a sessions should be gone for i := range 3 { - _, err := store.Get(fmt.Sprintf("sqlite-user-a-%d", i)) + _, err := store.Get(core.Sprintf("sqlite-user-a-%d", i)) assert.ErrorIs(t, err, ErrSessionNotFound) } @@ -296,7 +294,7 @@ func TestSQLiteSessionStore_Cleanup_Good(t *testing.T) { func TestSQLiteSessionStore_Persistence_Good(t *testing.T) { dir := t.TempDir() - dbPath := filepath.Join(dir, "sessions.db") + dbPath := core.Path(dir, "sessions.db") // Write a session store1, err := NewSQLiteSessionStore(dbPath) @@ -327,7 +325,7 @@ func TestSQLiteSessionStore_Persistence_Good(t *testing.T) { func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) { // Use a temp file — :memory: SQLite has concurrency limitations - dbPath := filepath.Join(t.TempDir(), "concurrent.db") + dbPath := core.Path(t.TempDir(), "concurrent.db") store, err := NewSQLiteSessionStore(dbPath) require.NoError(t, err) defer store.Close() @@ -339,11 +337,11 @@ func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) { for i := range n { go func(idx int) { defer wg.Done() - token := fmt.Sprintf("sqlite-concurrent-%d", idx) + token := core.Sprintf("sqlite-concurrent-%d", idx) err := store.Set(&Session{ Token: token, - UserID: fmt.Sprintf("user-%d", idx%5), + UserID: core.Sprintf("user-%d", idx%5), ExpiresAt: time.Now().Add(1 * time.Hour), }) assert.NoError(t, err) @@ -480,8 +478,7 @@ func TestSQLiteSessionStore_UpdateExisting_Good(t *testing.T) { func TestSQLiteSessionStore_TempFile_Good(t *testing.T) { // Verify we can use a real temp file (not :memory:) - tmpFile := filepath.Join(os.TempDir(), "go-crypt-test-session-store.db") - defer os.Remove(tmpFile) + tmpFile := core.Path(t.TempDir(), "go-crypt-test-session-store.db") store, err := NewSQLiteSessionStore(tmpFile) require.NoError(t, err) diff --git a/cmd/crypt/cmd_checksum.go b/cmd/crypt/cmd_checksum.go index 0475fa5..e751f51 100644 --- a/cmd/crypt/cmd_checksum.go +++ b/cmd/crypt/cmd_checksum.go @@ -1,9 +1,7 @@ package crypt import ( - "fmt" - "path/filepath" - + core "dappco.re/go/core" "dappco.re/go/core/crypt/crypt" "forge.lthn.ai/core/cli/pkg/cli" ) @@ -42,12 +40,12 @@ func runChecksum(path string) error { if checksumVerify != "" { if hash == checksumVerify { - cli.Success(fmt.Sprintf("Checksum matches: %s", filepath.Base(path))) + cli.Success(core.Sprintf("Checksum matches: %s", core.PathBase(path))) return nil } - cli.Error(fmt.Sprintf("Checksum mismatch: %s", filepath.Base(path))) - cli.Dim(fmt.Sprintf(" expected: %s", checksumVerify)) - cli.Dim(fmt.Sprintf(" got: %s", hash)) + cli.Error(core.Sprintf("Checksum mismatch: %s", core.PathBase(path))) + cli.Dim(core.Sprintf(" expected: %s", checksumVerify)) + cli.Dim(core.Sprintf(" got: %s", hash)) return cli.Err("checksum verification failed") } @@ -56,6 +54,6 @@ func runChecksum(path string) error { algo = "SHA-512" } - fmt.Printf("%s %s (%s)\n", hash, path, algo) + core.Print(nil, "%s %s (%s)", hash, path, algo) return nil } diff --git a/cmd/crypt/cmd_encrypt.go b/cmd/crypt/cmd_encrypt.go index 709733d..44db894 100644 --- a/cmd/crypt/cmd_encrypt.go +++ b/cmd/crypt/cmd_encrypt.go @@ -1,9 +1,7 @@ package crypt import ( - "fmt" - "strings" - + core "dappco.re/go/core" "dappco.re/go/core/crypt/crypt" coreio "dappco.re/go/core/io" "forge.lthn.ai/core/cli/pkg/cli" @@ -74,7 +72,7 @@ func runEncrypt(path string) error { return cli.Wrap(err, "failed to write encrypted file") } - cli.Success(fmt.Sprintf("Encrypted %s -> %s", path, outPath)) + cli.Success(core.Sprintf("Encrypted %s -> %s", path, outPath)) return nil } @@ -103,7 +101,7 @@ func runDecrypt(path string) error { return cli.Wrap(err, "failed to decrypt") } - outPath := strings.TrimSuffix(path, ".enc") + outPath := core.TrimSuffix(path, ".enc") if outPath == path { outPath = path + ".dec" } @@ -112,6 +110,6 @@ func runDecrypt(path string) error { return cli.Wrap(err, "failed to write decrypted file") } - cli.Success(fmt.Sprintf("Decrypted %s -> %s", path, outPath)) + cli.Success(core.Sprintf("Decrypted %s -> %s", path, outPath)) return nil } diff --git a/cmd/crypt/cmd_hash.go b/cmd/crypt/cmd_hash.go index d8fca7b..ca3cbe2 100644 --- a/cmd/crypt/cmd_hash.go +++ b/cmd/crypt/cmd_hash.go @@ -1,8 +1,7 @@ package crypt import ( - "fmt" - + core "dappco.re/go/core" "dappco.re/go/core/crypt/crypt" "forge.lthn.ai/core/cli/pkg/cli" @@ -39,7 +38,7 @@ func runHash(input string) error { if err != nil { return cli.Wrap(err, "failed to hash password") } - fmt.Println(hash) + core.Println(hash) return nil } @@ -47,7 +46,7 @@ func runHash(input string) error { if err != nil { return cli.Wrap(err, "failed to hash password") } - fmt.Println(hash) + core.Println(hash) return nil } diff --git a/cmd/crypt/cmd_keygen.go b/cmd/crypt/cmd_keygen.go index 025ebf5..0b752ae 100644 --- a/cmd/crypt/cmd_keygen.go +++ b/cmd/crypt/cmd_keygen.go @@ -4,8 +4,8 @@ import ( "crypto/rand" "encoding/base64" "encoding/hex" - "fmt" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" ) @@ -43,12 +43,12 @@ func runKeygen() error { switch { case keygenHex: - fmt.Println(hex.EncodeToString(key)) + core.Println(hex.EncodeToString(key)) case keygenBase64: - fmt.Println(base64.StdEncoding.EncodeToString(key)) + core.Println(base64.StdEncoding.EncodeToString(key)) default: // Default to hex output - fmt.Println(hex.EncodeToString(key)) + core.Println(hex.EncodeToString(key)) } return nil diff --git a/cmd/testcmd/cmd_output.go b/cmd/testcmd/cmd_output.go index f9ae73a..8665215 100644 --- a/cmd/testcmd/cmd_output.go +++ b/cmd/testcmd/cmd_output.go @@ -3,13 +3,11 @@ package testcmd import ( "bufio" "cmp" - "fmt" - "path/filepath" "regexp" "slices" "strconv" - "strings" + core "dappco.re/go/core" "dappco.re/go/core/i18n" ) @@ -40,7 +38,7 @@ func parseTestOutput(output string) testResults { skipPattern := regexp.MustCompile(`^\?\s+(\S+)\s+\[no test files\]`) coverPattern := regexp.MustCompile(`coverage:\s+([\d.]+)%`) - scanner := bufio.NewScanner(strings.NewReader(output)) + scanner := bufio.NewScanner(core.NewReader(output)) for scanner.Scan() { line := scanner.Text() @@ -85,21 +83,32 @@ func printTestSummary(results testResults, showCoverage bool) { // Print pass/fail summary total := results.passed + results.failed if total > 0 { - fmt.Printf(" %s %s", testPassStyle.Render("✓"), i18n.T("i18n.count.passed", results.passed)) + line := core.NewBuilder() + line.WriteString(" ") + line.WriteString(testPassStyle.Render("✓")) + line.WriteString(" ") + line.WriteString(i18n.T("i18n.count.passed", results.passed)) if results.failed > 0 { - fmt.Printf(" %s %s", testFailStyle.Render("✗"), i18n.T("i18n.count.failed", results.failed)) + line.WriteString(" ") + line.WriteString(testFailStyle.Render("✗")) + line.WriteString(" ") + line.WriteString(i18n.T("i18n.count.failed", results.failed)) } if results.skipped > 0 { - fmt.Printf(" %s %s", testSkipStyle.Render("○"), i18n.T("i18n.count.skipped", results.skipped)) + line.WriteString(" ") + line.WriteString(testSkipStyle.Render("○")) + line.WriteString(" ") + line.WriteString(i18n.T("i18n.count.skipped", results.skipped)) } - fmt.Println() + core.Println(line.String()) } // Print failed packages if len(results.failedPkgs) > 0 { - fmt.Printf("\n %s\n", i18n.T("cmd.test.failed_packages")) + core.Println() + core.Println(" " + i18n.T("cmd.test.failed_packages")) for _, pkg := range results.failedPkgs { - fmt.Printf(" %s %s\n", testFailStyle.Render("✗"), pkg) + core.Println(core.Sprintf(" %s %s", testFailStyle.Render("✗"), pkg)) } } @@ -108,7 +117,8 @@ func printTestSummary(results testResults, showCoverage bool) { printCoverageSummary(results) } else if results.covCount > 0 { avgCov := results.totalCov / float64(results.covCount) - fmt.Printf("\n %s %s\n", i18n.Label("coverage"), formatCoverage(avgCov)) + core.Println() + core.Println(core.Sprintf(" %s %s", i18n.Label("coverage"), formatCoverage(avgCov))) } } @@ -117,7 +127,8 @@ func printCoverageSummary(results testResults) { return } - fmt.Printf("\n %s\n", testHeaderStyle.Render(i18n.T("cmd.test.coverage_by_package"))) + core.Println() + core.Println(" " + testHeaderStyle.Render(i18n.T("cmd.test.coverage_by_package"))) // Sort packages by name slices.SortFunc(results.packages, func(a, b packageCoverage) int { @@ -143,8 +154,8 @@ func printCoverageSummary(results testResults) { if padLen < 0 { padLen = 2 } - padding := strings.Repeat(" ", padLen) - fmt.Printf(" %s%s%s\n", name, padding, formatCoverage(pkg.coverage)) + padding := repeatString(" ", padLen) + core.Println(core.Sprintf(" %s%s%s", name, padding, formatCoverage(pkg.coverage))) } // Print average @@ -155,13 +166,14 @@ func printCoverageSummary(results testResults) { if padLen < 0 { padLen = 2 } - padding := strings.Repeat(" ", padLen) - fmt.Printf("\n %s%s%s\n", testHeaderStyle.Render(avgLabel), padding, formatCoverage(avgCov)) + padding := repeatString(" ", padLen) + core.Println() + core.Println(core.Sprintf(" %s%s%s", testHeaderStyle.Render(avgLabel), padding, formatCoverage(avgCov))) } } func formatCoverage(cov float64) string { - s := fmt.Sprintf("%.1f%%", cov) + s := core.Sprintf("%.1f%%", cov) if cov >= 80 { return testCovHighStyle.Render(s) } else if cov >= 50 { @@ -172,41 +184,47 @@ func formatCoverage(cov float64) string { func shortenPackageName(name string) string { const modulePrefix = "dappco.re/go/" - if strings.HasPrefix(name, modulePrefix) { - remainder := strings.TrimPrefix(name, modulePrefix) - // If there's a sub-path (e.g. "go/pkg/foo"), strip the module name - if idx := strings.Index(remainder, "/"); idx >= 0 { - return remainder[idx+1:] + if core.HasPrefix(name, modulePrefix) { + remainder := core.TrimPrefix(name, modulePrefix) + parts := core.SplitN(remainder, "/", 2) + if len(parts) == 2 { + return parts[1] } // Module root (e.g. "cli-php") — return as-is return remainder } - return filepath.Base(name) + return core.PathBase(name) } func printJSONResults(results testResults, exitCode int) { - // Simple JSON output for agents - fmt.Printf("{\n") - fmt.Printf(" \"passed\": %d,\n", results.passed) - fmt.Printf(" \"failed\": %d,\n", results.failed) - fmt.Printf(" \"skipped\": %d,\n", results.skipped) + payload := struct { + Passed int `json:"passed"` + Failed int `json:"failed"` + Skipped int `json:"skipped"` + Coverage float64 `json:"coverage,omitempty"` + ExitCode int `json:"exit_code"` + FailedPackages []string `json:"failed_packages"` + }{ + Passed: results.passed, + Failed: results.failed, + Skipped: results.skipped, + ExitCode: exitCode, + FailedPackages: results.failedPkgs, + } if results.covCount > 0 { - avgCov := results.totalCov / float64(results.covCount) - fmt.Printf(" \"coverage\": %.1f,\n", avgCov) + payload.Coverage = results.totalCov / float64(results.covCount) } - fmt.Printf(" \"exit_code\": %d,\n", exitCode) - if len(results.failedPkgs) > 0 { - fmt.Printf(" \"failed_packages\": [\n") - for i, pkg := range results.failedPkgs { - comma := "," - if i == len(results.failedPkgs)-1 { - comma = "" - } - fmt.Printf(" %q%s\n", pkg, comma) - } - fmt.Printf(" ]\n") - } else { - fmt.Printf(" \"failed_packages\": []\n") - } - fmt.Printf("}\n") + core.Println(core.JSONMarshalString(payload)) +} + +func repeatString(part string, count int) string { + if count <= 0 { + return "" + } + + builder := core.NewBuilder() + for range count { + builder.WriteString(part) + } + return builder.String() } diff --git a/cmd/testcmd/cmd_runner.go b/cmd/testcmd/cmd_runner.go index d952b1b..6195458 100644 --- a/cmd/testcmd/cmd_runner.go +++ b/cmd/testcmd/cmd_runner.go @@ -2,20 +2,31 @@ package testcmd import ( "bufio" - "fmt" - "io" - "os" - "os/exec" + "context" "runtime" - "strings" + "sync" + core "dappco.re/go/core" "dappco.re/go/core/i18n" coreerr "dappco.re/go/core/log" + "dappco.re/go/core/process" +) + +var ( + processInitOnce sync.Once + processInitErr error ) func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bool) error { + processInitOnce.Do(func() { + processInitErr = process.Init(core.New()) + }) + if processInitErr != nil { + return coreerr.E("cmd.test", i18n.T("i18n.fail.run", "tests"), processInitErr) + } + // Detect if we're in a Go project - if _, err := os.Stat("go.mod"); os.IsNotExist(err) { + if !(&core.Fs{}).New("/").Exists("go.mod") { return coreerr.E("cmd.test", i18n.T("cmd.test.error.no_go_mod"), nil) } @@ -47,45 +58,32 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo // Add package pattern args = append(args, pkg) - // Create command - cmd := exec.Command("go", args...) - cmd.Dir, _ = os.Getwd() - - // Set environment to suppress macOS linker warnings - cmd.Env = append(os.Environ(), getMacOSDeploymentTarget()) - if !jsonOutput { - fmt.Printf("%s %s\n", testHeaderStyle.Render(i18n.Label("test")), i18n.ProgressSubject("run", "tests")) - fmt.Printf(" %s %s\n", i18n.Label("package"), testDimStyle.Render(pkg)) + core.Println(core.Sprintf("%s %s", testHeaderStyle.Render(i18n.Label("test")), i18n.ProgressSubject("run", "tests"))) + core.Println(core.Sprintf(" %s %s", i18n.Label("package"), testDimStyle.Render(pkg))) if run != "" { - fmt.Printf(" %s %s\n", i18n.Label("filter"), testDimStyle.Render(run)) + core.Println(core.Sprintf(" %s %s", i18n.Label("filter"), testDimStyle.Render(run))) } - fmt.Println() + core.Println() } - // Capture output for parsing - var stdout, stderr strings.Builder - - if verbose && !jsonOutput { - // Stream output in verbose mode, but also capture for parsing - cmd.Stdout = io.MultiWriter(os.Stdout, &stdout) - cmd.Stderr = io.MultiWriter(os.Stderr, &stderr) - } else { - // Capture output for parsing - cmd.Stdout = &stdout - cmd.Stderr = &stderr + options := process.RunOptions{ + Command: "go", + Args: args, + Dir: core.Env("DIR_CWD"), + } + if target := getMacOSDeploymentTarget(); target != "" { + options.Env = []string{target} } - err := cmd.Run() - exitCode := 0 + proc, err := process.StartWithOptions(context.Background(), options) if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - exitCode = exitErr.ExitCode() - } + return coreerr.E("cmd.test", i18n.T("i18n.fail.run", "tests"), err) } - // Combine stdout and stderr for parsing, filtering linker warnings - combined := filterLinkerWarnings(stdout.String() + "\n" + stderr.String()) + waitErr := proc.Wait() + exitCode := proc.ExitCode + combined := filterLinkerWarnings(proc.Output()) // Parse results results := parseTestOutput(combined) @@ -104,16 +102,23 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo printTestSummary(results, coverage) } else if coverage { // In verbose mode, still show coverage summary at end - fmt.Println() + if combined != "" { + core.Println(combined) + } + core.Println() printCoverageSummary(results) + } else if combined != "" { + core.Println(combined) } if exitCode != 0 { - fmt.Printf("\n%s %s\n", testFailStyle.Render(i18n.T("cli.fail")), i18n.T("cmd.test.tests_failed")) - return coreerr.E("cmd.test", i18n.T("i18n.fail.run", "tests"), nil) + core.Println() + core.Println(core.Sprintf("%s %s", testFailStyle.Render(i18n.T("cli.fail")), i18n.T("cmd.test.tests_failed"))) + return coreerr.E("cmd.test", i18n.T("i18n.fail.run", "tests"), waitErr) } - fmt.Printf("\n%s %s\n", testPassStyle.Render(i18n.T("cli.pass")), i18n.T("common.result.all_passed")) + core.Println() + core.Println(core.Sprintf("%s %s", testPassStyle.Render(i18n.T("cli.pass")), i18n.T("common.result.all_passed"))) return nil } @@ -128,18 +133,18 @@ func getMacOSDeploymentTarget() string { func filterLinkerWarnings(output string) string { // Filter out ld: warning lines that pollute the output var filtered []string - scanner := bufio.NewScanner(strings.NewReader(output)) + scanner := bufio.NewScanner(core.NewReader(output)) for scanner.Scan() { line := scanner.Text() // Skip linker warnings - if strings.HasPrefix(line, "ld: warning:") { + if core.HasPrefix(line, "ld: warning:") { continue } // Skip test binary build comments - if strings.HasPrefix(line, "# ") && strings.HasSuffix(line, ".test") { + if core.HasPrefix(line, "# ") && core.HasSuffix(line, ".test") { continue } filtered = append(filtered, line) } - return strings.Join(filtered, "\n") + return core.Join("\n", filtered...) } diff --git a/crypt/chachapoly/chachapoly.go b/crypt/chachapoly/chachapoly.go index d5db0fa..733feaa 100644 --- a/crypt/chachapoly/chachapoly.go +++ b/crypt/chachapoly/chachapoly.go @@ -2,9 +2,9 @@ package chachapoly import ( "crypto/rand" - "fmt" "io" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "golang.org/x/crypto/chacha20poly1305" @@ -34,7 +34,7 @@ func Decrypt(ciphertext []byte, key []byte) ([]byte, error) { minLen := aead.NonceSize() + aead.Overhead() if len(ciphertext) < minLen { - return nil, coreerr.E("chachapoly.Decrypt", fmt.Sprintf("ciphertext too short: got %d bytes, need at least %d bytes", len(ciphertext), minLen), nil) + return nil, coreerr.E("chachapoly.Decrypt", core.Sprintf("ciphertext too short: got %d bytes, need at least %d bytes", len(ciphertext), minLen), nil) } nonce, ciphertext := ciphertext[:aead.NonceSize()], ciphertext[aead.NonceSize():] diff --git a/crypt/chachapoly/chachapoly_test.go b/crypt/chachapoly/chachapoly_test.go index da5b96c..8e2c548 100644 --- a/crypt/chachapoly/chachapoly_test.go +++ b/crypt/chachapoly/chachapoly_test.go @@ -2,9 +2,9 @@ package chachapoly import ( "crypto/rand" - "errors" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" ) @@ -12,7 +12,7 @@ import ( type mockReader struct{} func (r *mockReader) Read(p []byte) (n int, err error) { - return 0, errors.New("read error") + return 0, core.NewError("read error") } func TestEncryptDecrypt_Good(t *testing.T) { diff --git a/crypt/checksum.go b/crypt/checksum.go index 7f5c7a7..80b8af5 100644 --- a/crypt/checksum.go +++ b/crypt/checksum.go @@ -5,17 +5,19 @@ import ( "crypto/sha512" "encoding/hex" "io" - "os" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) // SHA256File computes the SHA-256 checksum of a file and returns it as a hex string. func SHA256File(path string) (string, error) { - f, err := os.Open(path) - if err != nil { + openResult := (&core.Fs{}).New("/").Open(path) + if !openResult.OK { + err, _ := openResult.Value.(error) return "", coreerr.E("crypt.SHA256File", "failed to open file", err) } + f := openResult.Value.(io.ReadCloser) defer func() { _ = f.Close() }() h := sha256.New() @@ -28,10 +30,12 @@ func SHA256File(path string) (string, error) { // SHA512File computes the SHA-512 checksum of a file and returns it as a hex string. func SHA512File(path string) (string, error) { - f, err := os.Open(path) - if err != nil { + openResult := (&core.Fs{}).New("/").Open(path) + if !openResult.OK { + err, _ := openResult.Value.(error) return "", coreerr.E("crypt.SHA512File", "failed to open file", err) } + f := openResult.Value.(io.ReadCloser) defer func() { _ = f.Close() }() h := sha512.New() diff --git a/crypt/checksum_test.go b/crypt/checksum_test.go index 3c50c1e..03f92bb 100644 --- a/crypt/checksum_test.go +++ b/crypt/checksum_test.go @@ -1,10 +1,9 @@ package crypt import ( - "os" - "path/filepath" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -30,9 +29,9 @@ func TestSHA512Sum_Good(t *testing.T) { // TestSHA256FileEmpty_Good verifies checksum of an empty file. func TestSHA256FileEmpty_Good(t *testing.T) { tmpDir := t.TempDir() - emptyFile := filepath.Join(tmpDir, "empty.bin") - err := os.WriteFile(emptyFile, []byte{}, 0o644) - require.NoError(t, err) + emptyFile := core.Path(tmpDir, "empty.bin") + writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644) + require.Truef(t, writeResult.OK, "failed to write empty test file: %v", writeResult.Value) hash, err := SHA256File(emptyFile) require.NoError(t, err) @@ -43,9 +42,9 @@ func TestSHA256FileEmpty_Good(t *testing.T) { // TestSHA512FileEmpty_Good verifies SHA-512 checksum of an empty file. func TestSHA512FileEmpty_Good(t *testing.T) { tmpDir := t.TempDir() - emptyFile := filepath.Join(tmpDir, "empty.bin") - err := os.WriteFile(emptyFile, []byte{}, 0o644) - require.NoError(t, err) + emptyFile := core.Path(tmpDir, "empty.bin") + writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644) + require.Truef(t, writeResult.OK, "failed to write empty test file: %v", writeResult.Value) hash, err := SHA512File(emptyFile) require.NoError(t, err) @@ -69,9 +68,9 @@ func TestSHA512FileNonExistent_Bad(t *testing.T) { // TestSHA256FileWithContent_Good verifies checksum of a file with known content. func TestSHA256FileWithContent_Good(t *testing.T) { tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "test.txt") - err := os.WriteFile(testFile, []byte("hello"), 0o644) - require.NoError(t, err) + testFile := core.Path(tmpDir, "test.txt") + writeResult := (&core.Fs{}).New("/").WriteMode(testFile, "hello", 0o644) + require.Truef(t, writeResult.OK, "failed to write checksum fixture: %v", writeResult.Value) hash, err := SHA256File(testFile) require.NoError(t, err) diff --git a/crypt/hash.go b/crypt/hash.go index 80b2127..bd08003 100644 --- a/crypt/hash.go +++ b/crypt/hash.go @@ -3,9 +3,9 @@ package crypt import ( "crypto/subtle" "encoding/base64" - "fmt" - "strings" + "strconv" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "golang.org/x/crypto/argon2" @@ -25,7 +25,7 @@ func HashPassword(password string) (string, error) { b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) - encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + encoded := core.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, argon2Memory, argon2Time, argon2Parallelism, b64Salt, b64Hash) @@ -35,20 +35,21 @@ func HashPassword(password string) (string, error) { // VerifyPassword verifies a password against an Argon2id hash string. // The hash must be in the format produced by HashPassword. func VerifyPassword(password, hash string) (bool, error) { - parts := strings.Split(hash, "$") + parts := core.Split(hash, "$") if len(parts) != 6 { return false, coreerr.E("crypt.VerifyPassword", "invalid hash format", nil) } - var version int - if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil { + version, err := parsePrefixedInt(parts[2], "v=") + if err != nil { return false, coreerr.E("crypt.VerifyPassword", "failed to parse version", err) } + if version != argon2.Version { + return false, coreerr.E("crypt.VerifyPassword", core.Sprintf("unsupported argon2 version %d", version), nil) + } - var memory uint32 - var time uint32 - var parallelism uint8 - if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, ¶llelism); err != nil { + memory, time, parallelism, err := parseArgonParams(parts[3]) + if err != nil { return false, coreerr.E("crypt.VerifyPassword", "failed to parse parameters", err) } @@ -67,6 +68,52 @@ func VerifyPassword(password, hash string) (bool, error) { return subtle.ConstantTimeCompare(computedHash, expectedHash) == 1, nil } +func parseArgonParams(input string) (uint32, uint32, uint8, error) { + fields := core.Split(input, ",") + if len(fields) != 3 { + return 0, 0, 0, core.NewError("invalid argon2 parameters") + } + + memory, err := parsePrefixedUint32(fields[0], "m=") + if err != nil { + return 0, 0, 0, err + } + time, err := parsePrefixedUint32(fields[1], "t=") + if err != nil { + return 0, 0, 0, err + } + parallelismValue, err := parsePrefixedUint32(fields[2], "p=") + if err != nil { + return 0, 0, 0, err + } + + return memory, time, uint8(parallelismValue), nil +} + +func parsePrefixedInt(input, prefix string) (int, error) { + if !core.HasPrefix(input, prefix) { + return 0, core.NewError(core.Sprintf("missing %q prefix", prefix)) + } + + value, err := strconv.Atoi(core.TrimPrefix(input, prefix)) + if err != nil { + return 0, err + } + return value, nil +} + +func parsePrefixedUint32(input, prefix string) (uint32, error) { + if !core.HasPrefix(input, prefix) { + return 0, core.NewError(core.Sprintf("missing %q prefix", prefix)) + } + + value, err := strconv.ParseUint(core.TrimPrefix(input, prefix), 10, 32) + if err != nil { + return 0, err + } + return uint32(value), nil +} + // HashBcrypt hashes a password using bcrypt with the given cost. // Cost must be between bcrypt.MinCost and bcrypt.MaxCost. func HashBcrypt(password string, cost int) (string, error) { diff --git a/crypt/openpgp/service.go b/crypt/openpgp/service.go index cc63e26..5e1f85f 100644 --- a/crypt/openpgp/service.go +++ b/crypt/openpgp/service.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto" goio "io" - "strings" framework "dappco.re/go/core" coreerr "dappco.re/go/core/log" @@ -102,7 +101,7 @@ func serializeEntity(w goio.Writer, e *openpgp.Entity) error { // EncryptPGP encrypts data for a recipient identified by their public key (armored string in recipientPath). // The encrypted data is written to the provided writer and also returned as an armored string. func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opts ...any) (string, error) { - entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(recipientPath)) + entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(recipientPath)) if err != nil { return "", coreerr.E("openpgp.EncryptPGP", "failed to read recipient key", err) } @@ -137,7 +136,7 @@ func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opt // DecryptPGP decrypts a PGP message using the provided armored private key and passphrase. func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any) (string, error) { - entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(privateKey)) + entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(privateKey)) if err != nil { return "", coreerr.E("openpgp.DecryptPGP", "failed to read private key", err) } @@ -154,7 +153,7 @@ func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any } // Decrypt armored message - block, err := armor.Decode(strings.NewReader(message)) + block, err := armor.Decode(framework.NewReader(message)) if err != nil { return "", coreerr.E("openpgp.DecryptPGP", "failed to decode armored message", err) } diff --git a/crypt/rsa/rsa.go b/crypt/rsa/rsa.go index 93bc3be..a6534e9 100644 --- a/crypt/rsa/rsa.go +++ b/crypt/rsa/rsa.go @@ -6,8 +6,8 @@ import ( "crypto/sha256" "crypto/x509" "encoding/pem" - "fmt" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -24,7 +24,7 @@ func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err e const op = "rsa.GenerateKeyPair" if bits < 2048 { - return nil, nil, coreerr.E(op, fmt.Sprintf("key size too small: %d (minimum 2048)", bits), nil) + return nil, nil, coreerr.E(op, core.Sprintf("key size too small: %d (minimum 2048)", bits), nil) } privKey, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { diff --git a/crypt/rsa/rsa_test.go b/crypt/rsa/rsa_test.go index 7211995..8ee7460 100644 --- a/crypt/rsa/rsa_test.go +++ b/crypt/rsa/rsa_test.go @@ -6,9 +6,9 @@ import ( "crypto/rand" "crypto/x509" "encoding/pem" - "errors" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" ) @@ -16,7 +16,7 @@ import ( type mockReader struct{} func (r *mockReader) Read(p []byte) (n int, err error) { - return 0, errors.New("read error") + return 0, core.NewError("read error") } func TestRSA_Good(t *testing.T) { diff --git a/go.mod b/go.mod index ac2c01f..4a65ef2 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module dappco.re/go/core/crypt go 1.26.0 require ( - dappco.re/go/core v0.5.0 + dappco.re/go/core v0.8.0-alpha.1 dappco.re/go/core/i18n v0.2.0 dappco.re/go/core/io v0.2.0 dappco.re/go/core/log v0.1.0 + dappco.re/go/core/process v0.3.0 dappco.re/go/core/store v0.2.0 forge.lthn.ai/core/cli v0.3.7 github.com/ProtonMail/go-crypto v1.4.0 @@ -48,7 +49,6 @@ require ( github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/mod v0.34.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/text v0.35.0 // indirect diff --git a/go.sum b/go.sum index e5bd4d3..f34e190 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,13 @@ -dappco.re/go/core v0.5.0 h1:P5DJoaCiK5Q+af5UiTdWqUIW4W4qYKzpgGK50thm21U= -dappco.re/go/core v0.5.0/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= +dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= +dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= dappco.re/go/core/i18n v0.2.0 h1:NHzk6RCU93/qVRA3f2jvMr9P1R6FYheR/sHL+TnvKbI= dappco.re/go/core/i18n v0.2.0/go.mod h1:9eSVJXr3OpIGWQvDynfhqcp27xnLMwlYLgsByU+p7ok= dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4= dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E= dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc= dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= +dappco.re/go/core/process v0.3.0 h1:BPF9R79+8ZWe34qCIy/sZy+P4HwbaO95js2oPJL7IqM= +dappco.re/go/core/process v0.3.0/go.mod h1:qwx8kt6x+J9gn7fu8lavuess72Ye9jPBODqDZQ9K0as= dappco.re/go/core/store v0.2.0 h1:MH3R9m3mdr5T3lMWi37ryvTrXzF4xLBTYBGyNZF0p3I= dappco.re/go/core/store v0.2.0/go.mod h1:QQGJiruayjna3nywbf0N2gcO502q/oEkPoSpBpSKbLM= forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg= diff --git a/trust/approval.go b/trust/approval.go index fbf6bbd..e1f0495 100644 --- a/trust/approval.go +++ b/trust/approval.go @@ -1,11 +1,11 @@ package trust import ( - "fmt" "iter" "sync" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -31,7 +31,7 @@ func (s ApprovalStatus) String() string { case ApprovalDenied: return "denied" default: - return fmt.Sprintf("unknown(%d)", int(s)) + return core.Sprintf("unknown(%d)", int(s)) } } @@ -85,7 +85,7 @@ func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (strin defer q.mu.Unlock() q.nextID++ - id := fmt.Sprintf("approval-%d", q.nextID) + id := core.Sprintf("approval-%d", q.nextID) q.requests[id] = &ApprovalRequest{ ID: id, @@ -107,10 +107,10 @@ func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) err req, ok := q.requests[id] if !ok { - return coreerr.E("trust.ApprovalQueue.Approve", fmt.Sprintf("request %q not found", id), nil) + return coreerr.E("trust.ApprovalQueue.Approve", core.Sprintf("request %q not found", id), nil) } if req.Status != ApprovalPending { - return coreerr.E("trust.ApprovalQueue.Approve", fmt.Sprintf("request %q is already %s", id, req.Status), nil) + return coreerr.E("trust.ApprovalQueue.Approve", core.Sprintf("request %q is already %s", id, req.Status), nil) } req.Status = ApprovalApproved @@ -128,10 +128,10 @@ func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error req, ok := q.requests[id] if !ok { - return coreerr.E("trust.ApprovalQueue.Deny", fmt.Sprintf("request %q not found", id), nil) + return coreerr.E("trust.ApprovalQueue.Deny", core.Sprintf("request %q not found", id), nil) } if req.Status != ApprovalPending { - return coreerr.E("trust.ApprovalQueue.Deny", fmt.Sprintf("request %q is already %s", id, req.Status), nil) + return coreerr.E("trust.ApprovalQueue.Deny", core.Sprintf("request %q is already %s", id, req.Status), nil) } req.Status = ApprovalDenied diff --git a/trust/approval_test.go b/trust/approval_test.go index 23fe6f2..6e3c344 100644 --- a/trust/approval_test.go +++ b/trust/approval_test.go @@ -1,10 +1,10 @@ package trust import ( - "fmt" "sync" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -234,7 +234,7 @@ func TestApprovalConcurrent_Good(t *testing.T) { go func(idx int) { defer wg.Done() id, err := q.Submit( - fmt.Sprintf("agent-%d", idx), + core.Sprintf("agent-%d", idx), CapMergePR, "host-uk/core", ) diff --git a/trust/audit.go b/trust/audit.go index a9445f7..06b9032 100644 --- a/trust/audit.go +++ b/trust/audit.go @@ -1,12 +1,12 @@ package trust import ( - "encoding/json" "io" "iter" "sync" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -28,13 +28,20 @@ type AuditEntry struct { // MarshalJSON implements custom JSON encoding for Decision. func (d Decision) MarshalJSON() ([]byte, error) { - return json.Marshal(d.String()) + result := core.JSONMarshal(d.String()) + if !result.OK { + err, _ := result.Value.(error) + return nil, err + } + return result.Value.([]byte), nil } // UnmarshalJSON implements custom JSON decoding for Decision. func (d *Decision) UnmarshalJSON(data []byte) error { var s string - if err := json.Unmarshal(data, &s); err != nil { + result := core.JSONUnmarshal(data, &s) + if !result.OK { + err, _ := result.Value.(error) return err } switch s { @@ -82,11 +89,12 @@ func (l *AuditLog) Record(result EvalResult, repo string) error { l.entries = append(l.entries, entry) if l.writer != nil { - data, err := json.Marshal(entry) - if err != nil { + dataResult := core.JSONMarshal(entry) + if !dataResult.OK { + err, _ := dataResult.Value.(error) return coreerr.E("trust.AuditLog.Record", "marshal failed", err) } - data = append(data, '\n') + data := append(dataResult.Value.([]byte), '\n') if _, err := l.writer.Write(data); err != nil { return coreerr.E("trust.AuditLog.Record", "write failed", err) } diff --git a/trust/audit_test.go b/trust/audit_test.go index 2b459ed..23c9342 100644 --- a/trust/audit_test.go +++ b/trust/audit_test.go @@ -1,14 +1,11 @@ package trust import ( - "bytes" - "encoding/json" - "fmt" "io" - "strings" "sync" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -91,7 +88,7 @@ func TestAuditEntries_Good_AppendOnly(t *testing.T) { for i := range 5 { log.Record(EvalResult{ - Agent: fmt.Sprintf("agent-%d", i), + Agent: core.Sprintf("agent-%d", i), Cap: CapPushRepo, Decision: Allow, Reason: "ok", @@ -146,8 +143,8 @@ func TestAuditEntriesFor_Bad_NotFound(t *testing.T) { // --- Writer output --- func TestAuditRecord_Good_WritesToWriter(t *testing.T) { - var buf bytes.Buffer - log := NewAuditLog(&buf) + buf := core.NewBuilder() + log := NewAuditLog(buf) result := EvalResult{ Decision: Allow, @@ -160,11 +157,11 @@ func TestAuditRecord_Good_WritesToWriter(t *testing.T) { // Should have written a JSON line. output := buf.String() - assert.True(t, strings.HasSuffix(output, "\n")) + assert.True(t, core.HasSuffix(output, "\n")) var entry AuditEntry - err = json.Unmarshal([]byte(output), &entry) - require.NoError(t, err) + decodeResult := core.JSONUnmarshal([]byte(output), &entry) + require.Truef(t, decodeResult.OK, "failed to unmarshal audit entry: %v", decodeResult.Value) assert.Equal(t, "Athena", entry.Agent) assert.Equal(t, CapPushRepo, entry.Cap) assert.Equal(t, Allow, entry.Decision) @@ -172,26 +169,26 @@ func TestAuditRecord_Good_WritesToWriter(t *testing.T) { } func TestAuditRecord_Good_MultipleLines(t *testing.T) { - var buf bytes.Buffer - log := NewAuditLog(&buf) + buf := core.NewBuilder() + log := NewAuditLog(buf) for i := range 3 { log.Record(EvalResult{ - Agent: fmt.Sprintf("agent-%d", i), + Agent: core.Sprintf("agent-%d", i), Cap: CapPushRepo, Decision: Allow, Reason: "ok", }, "") } - lines := strings.Split(strings.TrimSpace(buf.String()), "\n") + lines := core.Split(core.Trim(buf.String()), "\n") assert.Len(t, lines, 3) // Each line should be valid JSON. for _, line := range lines { var entry AuditEntry - err := json.Unmarshal([]byte(line), &entry) - assert.NoError(t, err) + result := core.JSONUnmarshal([]byte(line), &entry) + assert.Truef(t, result.OK, "failed to unmarshal audit line: %v", result.Value) } } @@ -226,35 +223,37 @@ func TestDecisionJSON_Good_RoundTrip(t *testing.T) { expected := []string{`"deny"`, `"allow"`, `"needs_approval"`} for i, d := range decisions { - data, err := json.Marshal(d) - require.NoError(t, err) - assert.Equal(t, expected[i], string(data)) + result := core.JSONMarshal(d) + require.Truef(t, result.OK, "failed to marshal decision: %v", result.Value) + assert.Equal(t, expected[i], string(result.Value.([]byte))) var decoded Decision - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) + decodeResult := core.JSONUnmarshal(result.Value.([]byte), &decoded) + require.Truef(t, decodeResult.OK, "failed to unmarshal decision: %v", decodeResult.Value) assert.Equal(t, d, decoded) } } func TestDecisionJSON_Bad_UnknownString(t *testing.T) { var d Decision - err := json.Unmarshal([]byte(`"invalid"`), &d) + result := core.JSONUnmarshal([]byte(`"invalid"`), &d) + err, _ := result.Value.(error) assert.Error(t, err) assert.Contains(t, err.Error(), "unknown decision") } func TestDecisionJSON_Bad_NonString(t *testing.T) { var d Decision - err := json.Unmarshal([]byte(`42`), &d) + result := core.JSONUnmarshal([]byte(`42`), &d) + err, _ := result.Value.(error) assert.Error(t, err) } // --- Concurrent audit logging --- func TestAuditConcurrent_Good(t *testing.T) { - var buf bytes.Buffer - log := NewAuditLog(&buf) + buf := core.NewBuilder() + log := NewAuditLog(buf) const n = 10 var wg sync.WaitGroup @@ -264,7 +263,7 @@ func TestAuditConcurrent_Good(t *testing.T) { go func(idx int) { defer wg.Done() log.Record(EvalResult{ - Agent: fmt.Sprintf("agent-%d", idx), + Agent: core.Sprintf("agent-%d", idx), Cap: CapPushRepo, Decision: Allow, Reason: "ok", @@ -279,8 +278,8 @@ func TestAuditConcurrent_Good(t *testing.T) { // --- Integration: PolicyEngine + AuditLog --- func TestAuditPolicyIntegration_Good(t *testing.T) { - var buf bytes.Buffer - log := NewAuditLog(&buf) + buf := core.NewBuilder() + log := NewAuditLog(buf) pe := newTestEngine(t) // Evaluate and record diff --git a/trust/bench_test.go b/trust/bench_test.go index 6314567..142236d 100644 --- a/trust/bench_test.go +++ b/trust/bench_test.go @@ -1,8 +1,9 @@ package trust import ( - "fmt" "testing" + + core "dappco.re/go/core" ) // BenchmarkPolicyEvaluate measures policy evaluation across 100 registered agents. @@ -17,7 +18,7 @@ func BenchmarkPolicyEvaluate(b *testing.B) { tier = TierVerified } _ = r.Register(Agent{ - Name: fmt.Sprintf("agent-%d", i), + Name: core.Sprintf("agent-%d", i), Tier: tier, ScopedRepos: []string{"host-uk/core", "host-uk/docs"}, }) @@ -32,7 +33,7 @@ func BenchmarkPolicyEvaluate(b *testing.B) { b.ResetTimer() for i := range b.N { - agentName := fmt.Sprintf("agent-%d", i%100) + agentName := core.Sprintf("agent-%d", i%100) cap := caps[i%len(caps)] _ = pe.Evaluate(agentName, cap, "host-uk/core") } @@ -43,14 +44,14 @@ func BenchmarkRegistryGet(b *testing.B) { r := NewRegistry() for i := range 100 { _ = r.Register(Agent{ - Name: fmt.Sprintf("agent-%d", i), + Name: core.Sprintf("agent-%d", i), Tier: TierVerified, }) } b.ResetTimer() for i := range b.N { - name := fmt.Sprintf("agent-%d", i%100) + name := core.Sprintf("agent-%d", i%100) _ = r.Get(name) } } @@ -62,7 +63,7 @@ func BenchmarkRegistryRegister(b *testing.B) { b.ResetTimer() for i := range b.N { _ = r.Register(Agent{ - Name: fmt.Sprintf("bench-agent-%d", i), + Name: core.Sprintf("bench-agent-%d", i), Tier: TierVerified, }) } diff --git a/trust/config.go b/trust/config.go index 6f9a593..ea2618c 100644 --- a/trust/config.go +++ b/trust/config.go @@ -1,11 +1,9 @@ package trust import ( - "encoding/json" - "fmt" "io" - "os" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -24,20 +22,31 @@ type PoliciesConfig struct { // LoadPoliciesFromFile reads a JSON file and returns parsed policies. func LoadPoliciesFromFile(path string) ([]Policy, error) { - f, err := os.Open(path) - if err != nil { + openResult := (&core.Fs{}).New("/").Open(path) + if !openResult.OK { + err, _ := openResult.Value.(error) return nil, coreerr.E("trust.LoadPoliciesFromFile", "failed to open file", err) } - defer f.Close() - return LoadPolicies(f) + return LoadPolicies(openResult.Value.(io.Reader)) } // LoadPolicies reads JSON from a reader and returns parsed policies. func LoadPolicies(r io.Reader) ([]Policy, error) { + readResult := core.ReadAll(r) + if !readResult.OK { + err, _ := readResult.Value.(error) + return nil, coreerr.E("trust.LoadPolicies", "failed to decode JSON", err) + } + + data := []byte(readResult.Value.(string)) + if err := validatePoliciesJSON(data); err != nil { + return nil, coreerr.E("trust.LoadPolicies", "failed to decode JSON", err) + } + var cfg PoliciesConfig - dec := json.NewDecoder(r) - dec.DisallowUnknownFields() - if err := dec.Decode(&cfg); err != nil { + decodeResult := core.JSONUnmarshal(data, &cfg) + if !decodeResult.OK { + err, _ := decodeResult.Value.(error) return nil, coreerr.E("trust.LoadPolicies", "failed to decode JSON", err) } return convertPolicies(cfg) @@ -50,7 +59,7 @@ func convertPolicies(cfg PoliciesConfig) ([]Policy, error) { for i, pc := range cfg.Policies { tier := Tier(pc.Tier) if !tier.Valid() { - return nil, coreerr.E("trust.LoadPolicies", fmt.Sprintf("invalid tier %d at index %d", pc.Tier, i), nil) + return nil, coreerr.E("trust.LoadPolicies", core.Sprintf("invalid tier %d at index %d", pc.Tier, i), nil) } p := Policy{ @@ -82,12 +91,12 @@ func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error { // ApplyPoliciesFromFile loads policies from a JSON file and sets them on the engine. func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error { - f, err := os.Open(path) - if err != nil { + openResult := (&core.Fs{}).New("/").Open(path) + if !openResult.OK { + err, _ := openResult.Value.(error) return coreerr.E("trust.ApplyPoliciesFromFile", "failed to open file", err) } - defer f.Close() - return pe.ApplyPolicies(f) + return pe.ApplyPolicies(openResult.Value.(io.Reader)) } // ExportPolicies serialises the current policies as JSON to the given writer. @@ -106,14 +115,66 @@ func (pe *PolicyEngine) ExportPolicies(w io.Writer) error { }) } - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - if err := enc.Encode(cfg); err != nil { + dataResult := core.JSONMarshal(cfg) + if !dataResult.OK { + err, _ := dataResult.Value.(error) + return coreerr.E("trust.ExportPolicies", "failed to encode JSON", err) + } + if _, err := w.Write(dataResult.Value.([]byte)); err != nil { return coreerr.E("trust.ExportPolicies", "failed to encode JSON", err) } return nil } +func validatePoliciesJSON(data []byte) error { + var raw map[string]any + + result := core.JSONUnmarshal(data, &raw) + if !result.OK { + err, _ := result.Value.(error) + return err + } + + for key := range raw { + if key != "policies" { + return core.NewError(core.Sprintf("json: unknown field %q", key)) + } + } + + rawPolicies, ok := raw["policies"] + if !ok { + return nil + } + + policies, ok := rawPolicies.([]any) + if !ok { + return nil + } + + for _, rawPolicy := range policies { + fields, ok := rawPolicy.(map[string]any) + if !ok { + continue + } + for key := range fields { + if !isKnownPolicyConfigKey(key) { + return core.NewError(core.Sprintf("json: unknown field %q", key)) + } + } + } + + return nil +} + +func isKnownPolicyConfigKey(key string) bool { + switch key { + case "tier", "allowed", "requires_approval", "denied": + return true + default: + return false + } +} + // toCapabilities converts string slices to Capability slices. func toCapabilities(ss []string) []Capability { if len(ss) == 0 { diff --git a/trust/config_test.go b/trust/config_test.go index ef7f970..72825ba 100644 --- a/trust/config_test.go +++ b/trust/config_test.go @@ -1,13 +1,9 @@ package trust import ( - "bytes" - "encoding/json" - "os" - "path/filepath" - "strings" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -35,13 +31,13 @@ const validPolicyJSON = `{ // --- LoadPolicies --- func TestLoadPolicies_Good(t *testing.T) { - policies, err := LoadPolicies(strings.NewReader(validPolicyJSON)) + policies, err := LoadPolicies(core.NewReader(validPolicyJSON)) require.NoError(t, err) assert.Len(t, policies, 3) } func TestLoadPolicies_Good_FieldMapping(t *testing.T) { - policies, err := LoadPolicies(strings.NewReader(validPolicyJSON)) + policies, err := LoadPolicies(core.NewReader(validPolicyJSON)) require.NoError(t, err) // Tier 3 @@ -66,33 +62,33 @@ func TestLoadPolicies_Good_FieldMapping(t *testing.T) { func TestLoadPolicies_Good_EmptyPolicies(t *testing.T) { input := `{"policies": []}` - policies, err := LoadPolicies(strings.NewReader(input)) + policies, err := LoadPolicies(core.NewReader(input)) require.NoError(t, err) assert.Empty(t, policies) } func TestLoadPolicies_Bad_InvalidJSON(t *testing.T) { - _, err := LoadPolicies(strings.NewReader(`{invalid`)) + _, err := LoadPolicies(core.NewReader(`{invalid`)) assert.Error(t, err) } func TestLoadPolicies_Bad_InvalidTier(t *testing.T) { input := `{"policies": [{"tier": 0, "allowed": ["repo.push"]}]}` - _, err := LoadPolicies(strings.NewReader(input)) + _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } func TestLoadPolicies_Bad_TierTooHigh(t *testing.T) { input := `{"policies": [{"tier": 99, "allowed": ["repo.push"]}]}` - _, err := LoadPolicies(strings.NewReader(input)) + _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } func TestLoadPolicies_Bad_UnknownField(t *testing.T) { input := `{"policies": [{"tier": 1, "allowed": ["repo.push"], "bogus": true}]}` - _, err := LoadPolicies(strings.NewReader(input)) + _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err, "DisallowUnknownFields should reject unknown fields") } @@ -100,9 +96,8 @@ func TestLoadPolicies_Bad_UnknownField(t *testing.T) { func TestLoadPoliciesFromFile_Good(t *testing.T) { dir := t.TempDir() - path := filepath.Join(dir, "policies.json") - err := os.WriteFile(path, []byte(validPolicyJSON), 0644) - require.NoError(t, err) + path := core.Path(dir, "policies.json") + writePolicyFile(t, path, validPolicyJSON) policies, err := LoadPoliciesFromFile(path) require.NoError(t, err) @@ -122,7 +117,7 @@ func TestApplyPolicies_Good(t *testing.T) { pe := NewPolicyEngine(r) // Apply custom policies from JSON - err := pe.ApplyPolicies(strings.NewReader(validPolicyJSON)) + err := pe.ApplyPolicies(core.NewReader(validPolicyJSON)) require.NoError(t, err) // Verify the Tier 2 policy was replaced @@ -144,7 +139,7 @@ func TestApplyPolicies_Bad_InvalidJSON(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) - err := pe.ApplyPolicies(strings.NewReader(`{invalid`)) + err := pe.ApplyPolicies(core.NewReader(`{invalid`)) assert.Error(t, err) } @@ -153,7 +148,7 @@ func TestApplyPolicies_Bad_InvalidTier(t *testing.T) { pe := NewPolicyEngine(r) input := `{"policies": [{"tier": 0, "allowed": ["repo.push"]}]}` - err := pe.ApplyPolicies(strings.NewReader(input)) + err := pe.ApplyPolicies(core.NewReader(input)) assert.Error(t, err) } @@ -161,15 +156,14 @@ func TestApplyPolicies_Bad_InvalidTier(t *testing.T) { func TestApplyPoliciesFromFile_Good(t *testing.T) { dir := t.TempDir() - path := filepath.Join(dir, "policies.json") - err := os.WriteFile(path, []byte(validPolicyJSON), 0644) - require.NoError(t, err) + path := core.Path(dir, "policies.json") + writePolicyFile(t, path, validPolicyJSON) r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "A", Tier: TierFull})) pe := NewPolicyEngine(r) - err = pe.ApplyPoliciesFromFile(path) + err := pe.ApplyPoliciesFromFile(path) require.NoError(t, err) // Verify Tier 3 was replaced — only 3 allowed caps now @@ -191,14 +185,14 @@ func TestExportPolicies_Good(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) // loads defaults - var buf bytes.Buffer - err := pe.ExportPolicies(&buf) + buf := core.NewBuilder() + err := pe.ExportPolicies(buf) require.NoError(t, err) // Output should be valid JSON var cfg PoliciesConfig - err = json.Unmarshal(buf.Bytes(), &cfg) - require.NoError(t, err) + result := core.JSONUnmarshalString(buf.String(), &cfg) + require.Truef(t, result.OK, "failed to unmarshal exported policies: %v", result.Value) assert.Len(t, cfg.Policies, 3) } @@ -208,15 +202,15 @@ func TestExportPolicies_Good_RoundTrip(t *testing.T) { pe := NewPolicyEngine(r) // Export - var buf bytes.Buffer - err := pe.ExportPolicies(&buf) + buf := core.NewBuilder() + err := pe.ExportPolicies(buf) require.NoError(t, err) // Create a new engine and apply the exported policies r2 := NewRegistry() require.NoError(t, r2.Register(Agent{Name: "A", Tier: TierFull})) pe2 := NewPolicyEngine(r2) - err = pe2.ApplyPolicies(strings.NewReader(buf.String())) + err = pe2.ApplyPolicies(core.NewReader(buf.String())) require.NoError(t, err) // Evaluations should produce the same results @@ -229,6 +223,13 @@ func TestExportPolicies_Good_RoundTrip(t *testing.T) { } } +func writePolicyFile(t *testing.T, path, content string) { + t.Helper() + + result := (&core.Fs{}).New("/").WriteMode(path, content, 0o644) + require.Truef(t, result.OK, "failed to write %s: %v", path, result.Value) +} + // --- Helper conversion --- func TestToCapabilities_Good(t *testing.T) { diff --git a/trust/policy.go b/trust/policy.go index 85ac374..bc72760 100644 --- a/trust/policy.go +++ b/trust/policy.go @@ -1,10 +1,9 @@ package trust import ( - "fmt" "slices" - "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -48,7 +47,7 @@ func (d Decision) String() string { case NeedsApproval: return "needs_approval" default: - return fmt.Sprintf("unknown(%d)", int(d)) + return core.Sprintf("unknown(%d)", int(d)) } } @@ -90,7 +89,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: Deny, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("no policy for tier %s", agent.Tier), + Reason: core.Sprintf("no policy for tier %s", agent.Tier), } } @@ -100,7 +99,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: Deny, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("capability %s is denied for tier %s", cap, agent.Tier), + Reason: core.Sprintf("capability %s is denied for tier %s", cap, agent.Tier), } } @@ -110,7 +109,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: NeedsApproval, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("capability %s requires approval for tier %s", cap, agent.Tier), + Reason: core.Sprintf("capability %s requires approval for tier %s", cap, agent.Tier), } } @@ -124,7 +123,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: Deny, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("agent %q does not have access to repo %q", agentName, repo), + Reason: core.Sprintf("agent %q does not have access to repo %q", agentName, repo), } } } @@ -132,7 +131,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: Allow, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("capability %s allowed for tier %s", cap, agent.Tier), + Reason: core.Sprintf("capability %s allowed for tier %s", cap, agent.Tier), } } } @@ -141,14 +140,14 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: Deny, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("capability %s not granted for tier %s", cap, agent.Tier), + Reason: core.Sprintf("capability %s not granted for tier %s", cap, agent.Tier), } } // SetPolicy replaces the policy for a given tier. func (pe *PolicyEngine) SetPolicy(p Policy) error { if !p.Tier.Valid() { - return coreerr.E("trust.SetPolicy", fmt.Sprintf("invalid tier %d", p.Tier), nil) + return coreerr.E("trust.SetPolicy", core.Sprintf("invalid tier %d", p.Tier), nil) } pe.policies[p.Tier] = &p return nil @@ -218,8 +217,8 @@ func (pe *PolicyEngine) loadDefaults() { // isRepoScoped returns true if the capability is constrained by repo scope. func isRepoScoped(cap Capability) bool { - return strings.HasPrefix(string(cap), "repo.") || - strings.HasPrefix(string(cap), "pr.") || + return core.HasPrefix(string(cap), "repo.") || + core.HasPrefix(string(cap), "pr.") || cap == CapReadSecrets } @@ -248,14 +247,14 @@ func matchScope(pattern, repo string) bool { } // Check for wildcard patterns. - if !strings.Contains(pattern, "*") { + if !core.Contains(pattern, "*") { return false } // "prefix/**" — recursive: matches anything under prefix/. - if strings.HasSuffix(pattern, "/**") { + if core.HasSuffix(pattern, "/**") { prefix := pattern[:len(pattern)-3] // strip "/**" - if !strings.HasPrefix(repo, prefix+"/") { + if !core.HasPrefix(repo, prefix+"/") { return false } // Must have something after the prefix/. @@ -263,14 +262,14 @@ func matchScope(pattern, repo string) bool { } // "prefix/*" — single level: matches prefix/X but not prefix/X/Y. - if strings.HasSuffix(pattern, "/*") { + if core.HasSuffix(pattern, "/*") { prefix := pattern[:len(pattern)-2] // strip "/*" - if !strings.HasPrefix(repo, prefix+"/") { + if !core.HasPrefix(repo, prefix+"/") { return false } remainder := repo[len(prefix)+1:] // Must have a non-empty name, and no further slashes. - return remainder != "" && !strings.Contains(remainder, "/") + return remainder != "" && !core.Contains(remainder, "/") } // Unsupported wildcard position — fall back to no match. diff --git a/trust/trust.go b/trust/trust.go index 3b32de0..8cc205f 100644 --- a/trust/trust.go +++ b/trust/trust.go @@ -11,11 +11,11 @@ package trust import ( - "fmt" "iter" "sync" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -41,7 +41,7 @@ func (t Tier) String() string { case TierFull: return "full" default: - return fmt.Sprintf("unknown(%d)", int(t)) + return core.Sprintf("unknown(%d)", int(t)) } } @@ -102,7 +102,7 @@ func (r *Registry) Register(agent Agent) error { return coreerr.E("trust.Register", "agent name is required", nil) } if !agent.Tier.Valid() { - return coreerr.E("trust.Register", fmt.Sprintf("invalid tier %d for agent %q", agent.Tier, agent.Name), nil) + return coreerr.E("trust.Register", core.Sprintf("invalid tier %d for agent %q", agent.Tier, agent.Name), nil) } if agent.CreatedAt.IsZero() { agent.CreatedAt = time.Now() diff --git a/trust/trust_test.go b/trust/trust_test.go index e323110..303facc 100644 --- a/trust/trust_test.go +++ b/trust/trust_test.go @@ -1,11 +1,11 @@ package trust import ( - "fmt" "sync" "testing" "time" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -196,7 +196,7 @@ func TestConcurrentRegistryOperations_Good(t *testing.T) { for i := range n { go func(idx int) { defer wg.Done() - name := fmt.Sprintf("agent-%d", idx) + name := core.Sprintf("agent-%d", idx) err := r.Register(Agent{Name: name, Tier: TierVerified}) assert.NoError(t, err) }(i) @@ -206,7 +206,7 @@ func TestConcurrentRegistryOperations_Good(t *testing.T) { for i := range n { go func(idx int) { defer wg.Done() - name := fmt.Sprintf("agent-%d", idx) + name := core.Sprintf("agent-%d", idx) _ = r.Get(name) // Just exercise the read path }(i) } @@ -215,7 +215,7 @@ func TestConcurrentRegistryOperations_Good(t *testing.T) { for i := range n { go func(idx int) { defer wg.Done() - name := fmt.Sprintf("agent-%d", idx) + name := core.Sprintf("agent-%d", idx) _ = r.Remove(name) }(i) } @@ -281,7 +281,7 @@ func TestConcurrentListDuringMutations_Good(t *testing.T) { // Pre-populate for i := range 5 { require.NoError(t, r.Register(Agent{ - Name: fmt.Sprintf("base-%d", i), + Name: core.Sprintf("base-%d", i), Tier: TierFull, })) } @@ -302,7 +302,7 @@ func TestConcurrentListDuringMutations_Good(t *testing.T) { for i := range 10 { go func(idx int) { defer wg.Done() - name := fmt.Sprintf("concurrent-%d", idx) + name := core.Sprintf("concurrent-%d", idx) _ = r.Register(Agent{Name: name, Tier: TierUntrusted}) }(i) } -- 2.45.3 From 53d7d59a9d19096e1577764a0ed5a3bc286411b5 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 26 Mar 2026 17:32:21 +0000 Subject: [PATCH 5/9] refactor(crypt): complete AX v0.8.0 polish pass Co-Authored-By: Virgil --- auth/auth.go | 34 +++++- auth/auth_test.go | 156 ++++++++++++++-------------- auth/hardware.go | 2 + auth/session_store.go | 9 ++ auth/session_store_sqlite.go | 8 ++ auth/session_store_test.go | 38 +++---- cmd/crypt/cmd.go | 1 + cmd/testcmd/cmd_main.go | 1 + cmd/testcmd/output_test.go | 8 +- crypt/chachapoly/chachapoly.go | 2 + crypt/chachapoly/chachapoly_test.go | 18 ++-- crypt/checksum.go | 4 + crypt/checksum_test.go | 24 ++--- crypt/crypt.go | 4 + crypt/crypt_test.go | 22 ++-- crypt/hash.go | 4 + crypt/hash_test.go | 6 +- crypt/hmac.go | 3 + crypt/hmac_test.go | 6 +- crypt/kdf.go | 3 + crypt/kdf_test.go | 18 ++-- crypt/lthn/lthn.go | 4 + crypt/lthn/lthn_test.go | 14 +-- crypt/openpgp/service.go | 6 ++ crypt/openpgp/service_test.go | 4 +- crypt/pgp/pgp.go | 6 ++ crypt/pgp/pgp_test.go | 24 ++--- crypt/rsa/rsa.go | 5 + crypt/rsa/rsa_test.go | 6 +- crypt/symmetric.go | 4 + crypt/symmetric_test.go | 26 ++--- trust/approval.go | 15 +++ trust/approval_test.go | 46 ++++---- trust/audit.go | 11 ++ trust/audit_test.go | 34 +++--- trust/config.go | 7 ++ trust/config_test.go | 40 +++---- trust/policy.go | 12 +++ trust/policy_test.go | 86 +++++++-------- trust/scope_test.go | 46 ++++---- trust/trust.go | 50 +++++++-- trust/trust_test.go | 64 ++++++------ 42 files changed, 527 insertions(+), 354 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index cf7417e..3291b27 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -40,11 +40,14 @@ import ( coreerr "dappco.re/go/core/log" ) -// Default durations for challenge and session lifetimes. const ( + // DefaultChallengeTTL is the default lifetime for a generated challenge. + // Usage: pass DefaultChallengeTTL into WithChallengeTTL(...) to keep the package default. DefaultChallengeTTL = 5 * time.Minute - DefaultSessionTTL = 24 * time.Hour - nonceBytes = 32 + // DefaultSessionTTL is the default lifetime for an authenticated session. + // Usage: pass DefaultSessionTTL into WithSessionTTL(...) to keep the package default. + DefaultSessionTTL = 24 * time.Hour + nonceBytes = 32 ) // protectedUsers lists usernames that cannot be deleted. @@ -55,6 +58,7 @@ var protectedUsers = map[string]bool{ } // User represents a registered user with PGP credentials. +// Usage: use User with the other exported helpers in this package. type User struct { PublicKey string `json:"public_key"` KeyID string `json:"key_id"` @@ -65,6 +69,7 @@ type User struct { } // Challenge is a PGP-encrypted nonce sent to a client during authentication. +// Usage: use Challenge with the other exported helpers in this package. type Challenge struct { Nonce []byte `json:"nonce"` Encrypted string `json:"encrypted"` // PGP-encrypted nonce (armored) @@ -72,6 +77,7 @@ type Challenge struct { } // Session represents an authenticated session. +// Usage: use Session with the other exported helpers in this package. type Session struct { Token string `json:"token"` UserID string `json:"user_id"` @@ -80,6 +86,7 @@ type Session struct { // Revocation records the details of a revoked user key. // Stored as JSON in the user's .rev file, replacing the legacy placeholder. +// Usage: use Revocation with the other exported helpers in this package. type Revocation struct { UserID string `json:"user_id"` Reason string `json:"reason"` @@ -87,9 +94,11 @@ type Revocation struct { } // Option configures an Authenticator. +// Usage: use Option with the other exported helpers in this package. type Option func(*Authenticator) // WithChallengeTTL sets the lifetime of a challenge before it expires. +// Usage: pass WithChallengeTTL(...) into the related constructor to adjust the default behaviour. func WithChallengeTTL(d time.Duration) Option { return func(a *Authenticator) { a.challengeTTL = d @@ -97,6 +106,7 @@ func WithChallengeTTL(d time.Duration) Option { } // WithSessionTTL sets the lifetime of a session before it expires. +// Usage: pass WithSessionTTL(...) into the related constructor to adjust the default behaviour. func WithSessionTTL(d time.Duration) Option { return func(a *Authenticator) { a.sessionTTL = d @@ -105,6 +115,7 @@ func WithSessionTTL(d time.Duration) Option { // WithSessionStore sets the SessionStore implementation. // If not provided, an in-memory store is used (sessions lost on restart). +// Usage: pass WithSessionStore(...) into the related constructor to adjust the default behaviour. func WithSessionStore(s SessionStore) Option { return func(a *Authenticator) { a.store = s @@ -120,6 +131,7 @@ func WithSessionStore(s SessionStore) Option { // An optional HardwareKey can be provided via WithHardwareKey for // hardware-backed cryptographic operations (PKCS#11, YubiKey, etc.). // See auth/hardware.go for the interface definition and integration points. +// Usage: create an Authenticator with New(...) and then call Register, Login, or CreateChallenge. type Authenticator struct { medium io.Medium store SessionStore @@ -133,6 +145,7 @@ type Authenticator struct { // New creates an Authenticator that persists user data via the given Medium. // By default, sessions are stored in memory. Use WithSessionStore to provide // a persistent implementation (e.g. SQLiteSessionStore). +// Usage: call New(...) to create a ready-to-use value. func New(m io.Medium, opts ...Option) *Authenticator { a := &Authenticator{ medium: m, @@ -159,6 +172,7 @@ func userPath(userID, ext string) string { // produce a userID, generates a PGP keypair (protected by the given password), // and persists the public key, private key, revocation placeholder, password // hash (Argon2id), and encrypted metadata via the Medium. +// Usage: call Register(...) during the package's normal workflow. func (a *Authenticator) Register(username, password string) (*User, error) { const op = "auth.Register" @@ -237,6 +251,7 @@ func (a *Authenticator) Register(username, password string) (*User, error) { // CreateChallenge generates a cryptographic challenge for the given user. // A random nonce is created and encrypted with the user's PGP public key. // The client must decrypt the nonce and sign it to prove key ownership. +// Usage: call CreateChallenge(...) during the package's normal workflow. func (a *Authenticator) CreateChallenge(userID string) (*Challenge, error) { const op = "auth.CreateChallenge" @@ -279,6 +294,7 @@ func (a *Authenticator) CreateChallenge(userID string) (*Challenge, error) { // ValidateResponse verifies a signed nonce from the client. The client must // have decrypted the challenge nonce and signed it with their private key. // On success, a new session is created and returned. +// Usage: call ValidateResponse(...) during the package's normal workflow. func (a *Authenticator) ValidateResponse(userID string, signedNonce []byte) (*Session, error) { const op = "auth.ValidateResponse" @@ -313,6 +329,7 @@ func (a *Authenticator) ValidateResponse(userID string, signedNonce []byte) (*Se } // ValidateSession checks whether a token maps to a valid, non-expired session. +// Usage: call ValidateSession(...) during the package's normal workflow. func (a *Authenticator) ValidateSession(token string) (*Session, error) { const op = "auth.ValidateSession" @@ -330,6 +347,7 @@ func (a *Authenticator) ValidateSession(token string) (*Session, error) { } // RefreshSession extends the expiry of an existing valid session. +// Usage: call RefreshSession(...) during the package's normal workflow. func (a *Authenticator) RefreshSession(token string) (*Session, error) { const op = "auth.RefreshSession" @@ -351,6 +369,7 @@ func (a *Authenticator) RefreshSession(token string) (*Session, error) { } // RevokeSession removes a session, invalidating the token immediately. +// Usage: call RevokeSession(...) during the package's normal workflow. func (a *Authenticator) RevokeSession(token string) error { const op = "auth.RevokeSession" @@ -363,6 +382,7 @@ func (a *Authenticator) RevokeSession(token string) error { // DeleteUser removes a user and all associated keys from storage. // The "server" user is protected and cannot be deleted (mirroring the // original TypeScript implementation's safeguard). +// Usage: call DeleteUser(...) during the package's normal workflow. func (a *Authenticator) DeleteUser(userID string) error { const op = "auth.DeleteUser" @@ -403,6 +423,8 @@ func (a *Authenticator) DeleteUser(userID string) error { // - Otherwise, falls back to legacy .lthn file with LTHN hash verification. // On successful legacy login, the password is re-hashed with Argon2id and // a .hash file is written (transparent migration). +// +// Usage: call Login(...) for password-based flows when challenge-response is not required. func (a *Authenticator) Login(userID, password string) (*Session, error) { const op = "auth.Login" @@ -455,6 +477,7 @@ func (a *Authenticator) Login(userID, password string) (*Session, error) { // all existing sessions. The caller must provide the current password // (oldPassword) to decrypt existing metadata and the new password (newPassword) // to protect the new keypair. +// Usage: call RotateKeyPair(...) during the package's normal workflow. func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) (*User, error) { const op = "auth.RotateKeyPair" @@ -539,6 +562,7 @@ func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) ( // RevokeKey marks a user's key as revoked. It verifies the password first, // writes a JSON revocation record to the .rev file (replacing the placeholder), // and invalidates all sessions for the user. +// Usage: call RevokeKey(...) during the package's normal workflow. func (a *Authenticator) RevokeKey(userID, password, reason string) error { const op = "auth.RevokeKey" @@ -576,6 +600,7 @@ func (a *Authenticator) RevokeKey(userID, password, reason string) error { // IsRevoked checks whether a user's key has been revoked by inspecting the // .rev file. Returns true only if the file contains valid revocation JSON // (not the legacy "REVOCATION_PLACEHOLDER" string). +// Usage: call IsRevoked(...) during the package's normal workflow. func (a *Authenticator) IsRevoked(userID string) bool { content, err := a.medium.Read(userPath(userID, ".rev")) if err != nil { @@ -601,6 +626,7 @@ func (a *Authenticator) IsRevoked(userID string) bool { // WriteChallengeFile writes an encrypted challenge to a file for air-gapped // (courier) transport. The challenge is created and then its encrypted nonce // is written to the specified path on the Medium. +// Usage: call WriteChallengeFile(...) during the package's normal workflow. func (a *Authenticator) WriteChallengeFile(userID, path string) error { const op = "auth.WriteChallengeFile" @@ -625,6 +651,7 @@ func (a *Authenticator) WriteChallengeFile(userID, path string) error { // ReadResponseFile reads a signed response from a file and validates it, // completing the air-gapped authentication flow. The file must contain the // raw PGP signature bytes (armored). +// Usage: call ReadResponseFile(...) during the package's normal workflow. func (a *Authenticator) ReadResponseFile(userID, path string) (*Session, error) { const op = "auth.ReadResponseFile" @@ -698,6 +725,7 @@ func (a *Authenticator) createSession(userID string) (*Session, error) { // StartCleanup runs a background goroutine that periodically removes expired // sessions from the store. It stops when the context is cancelled. +// Usage: call StartCleanup(...) during the package's normal workflow. func (a *Authenticator) StartCleanup(ctx context.Context, interval time.Duration) { go func() { ticker := time.NewTicker(interval) diff --git a/auth/auth_test.go b/auth/auth_test.go index 97a3c17..822a571 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -23,7 +23,7 @@ func newTestAuth(opts ...Option) (*Authenticator, *io.MockMedium) { // --- Register --- -func TestRegister_Good(t *testing.T) { +func TestAuth_Register_Good(t *testing.T) { a, m := newTestAuth() user, err := a.Register("alice", "hunter2") @@ -48,7 +48,7 @@ func TestRegister_Good(t *testing.T) { assert.False(t, user.Created.IsZero()) } -func TestRegister_Bad(t *testing.T) { +func TestAuth_Register_Bad(t *testing.T) { a, _ := newTestAuth() // Register first time succeeds @@ -61,7 +61,7 @@ func TestRegister_Bad(t *testing.T) { assert.Contains(t, err.Error(), "user already exists") } -func TestRegister_Ugly(t *testing.T) { +func TestAuth_Register_Ugly(t *testing.T) { a, _ := newTestAuth() // Empty username/password should still work (PGP allows it) @@ -72,7 +72,7 @@ func TestRegister_Ugly(t *testing.T) { // --- CreateChallenge --- -func TestCreateChallenge_Good(t *testing.T) { +func TestAuth_CreateChallenge_Good(t *testing.T) { a, _ := newTestAuth() user, err := a.Register("charlie", "pass") @@ -87,7 +87,7 @@ func TestCreateChallenge_Good(t *testing.T) { assert.True(t, challenge.ExpiresAt.After(time.Now())) } -func TestCreateChallenge_Bad(t *testing.T) { +func TestAuth_CreateChallenge_Bad(t *testing.T) { a, _ := newTestAuth() // Challenge for non-existent user @@ -96,7 +96,7 @@ func TestCreateChallenge_Bad(t *testing.T) { assert.Contains(t, err.Error(), "user not found") } -func TestCreateChallenge_Ugly(t *testing.T) { +func TestAuth_CreateChallenge_Ugly(t *testing.T) { a, _ := newTestAuth() // Empty userID @@ -106,7 +106,7 @@ func TestCreateChallenge_Ugly(t *testing.T) { // --- ValidateResponse (full challenge-response flow) --- -func TestValidateResponse_Good(t *testing.T) { +func TestAuth_ValidateResponse_Good(t *testing.T) { a, m := newTestAuth() // Register user @@ -140,7 +140,7 @@ func TestValidateResponse_Good(t *testing.T) { assert.True(t, session.ExpiresAt.After(time.Now())) } -func TestValidateResponse_Bad(t *testing.T) { +func TestAuth_ValidateResponse_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("eve", "pass") @@ -153,7 +153,7 @@ func TestValidateResponse_Bad(t *testing.T) { assert.Contains(t, err.Error(), "no pending challenge") } -func TestValidateResponse_Ugly(t *testing.T) { +func TestAuth_ValidateResponse_Ugly(t *testing.T) { a, m := newTestAuth(WithChallengeTTL(1 * time.Millisecond)) _, err := a.Register("frank", "pass") @@ -180,7 +180,7 @@ func TestValidateResponse_Ugly(t *testing.T) { // --- ValidateSession --- -func TestValidateSession_Good(t *testing.T) { +func TestAuth_ValidateSession_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("grace", "pass") @@ -196,7 +196,7 @@ func TestValidateSession_Good(t *testing.T) { assert.Equal(t, userID, validated.UserID) } -func TestValidateSession_Bad(t *testing.T) { +func TestAuth_ValidateSession_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.ValidateSession("nonexistent-token") @@ -204,7 +204,7 @@ func TestValidateSession_Bad(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestValidateSession_Ugly(t *testing.T) { +func TestAuth_ValidateSession_Ugly(t *testing.T) { a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond)) _, err := a.Register("heidi", "pass") @@ -223,7 +223,7 @@ func TestValidateSession_Ugly(t *testing.T) { // --- RefreshSession --- -func TestRefreshSession_Good(t *testing.T) { +func TestAuth_RefreshSession_Good(t *testing.T) { a, _ := newTestAuth(WithSessionTTL(1 * time.Hour)) _, err := a.Register("ivan", "pass") @@ -243,7 +243,7 @@ func TestRefreshSession_Good(t *testing.T) { assert.True(t, refreshed.ExpiresAt.After(originalExpiry)) } -func TestRefreshSession_Bad(t *testing.T) { +func TestAuth_RefreshSession_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.RefreshSession("nonexistent-token") @@ -251,7 +251,7 @@ func TestRefreshSession_Bad(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestRefreshSession_Ugly(t *testing.T) { +func TestAuth_RefreshSession_Ugly(t *testing.T) { a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond)) _, err := a.Register("judy", "pass") @@ -270,7 +270,7 @@ func TestRefreshSession_Ugly(t *testing.T) { // --- RevokeSession --- -func TestRevokeSession_Good(t *testing.T) { +func TestAuth_RevokeSession_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("karl", "pass") @@ -288,7 +288,7 @@ func TestRevokeSession_Good(t *testing.T) { assert.Error(t, err) } -func TestRevokeSession_Bad(t *testing.T) { +func TestAuth_RevokeSession_Bad(t *testing.T) { a, _ := newTestAuth() err := a.RevokeSession("nonexistent-token") @@ -296,7 +296,7 @@ func TestRevokeSession_Bad(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestRevokeSession_Ugly(t *testing.T) { +func TestAuth_RevokeSession_Ugly(t *testing.T) { a, _ := newTestAuth() // Revoke empty token @@ -306,7 +306,7 @@ func TestRevokeSession_Ugly(t *testing.T) { // --- DeleteUser --- -func TestDeleteUser_Good(t *testing.T) { +func TestAuth_DeleteUser_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("larry", "pass") @@ -334,7 +334,7 @@ func TestDeleteUser_Good(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestDeleteUser_Bad(t *testing.T) { +func TestAuth_DeleteUser_Bad(t *testing.T) { a, _ := newTestAuth() // Protected user "server" cannot be deleted @@ -343,7 +343,7 @@ func TestDeleteUser_Bad(t *testing.T) { assert.Contains(t, err.Error(), "cannot delete protected user") } -func TestDeleteUser_Ugly(t *testing.T) { +func TestAuth_DeleteUser_Ugly(t *testing.T) { a, _ := newTestAuth() // Non-existent user @@ -354,7 +354,7 @@ func TestDeleteUser_Ugly(t *testing.T) { // --- Login --- -func TestLogin_Good(t *testing.T) { +func TestAuth_Login_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("mallory", "secret") @@ -370,7 +370,7 @@ func TestLogin_Good(t *testing.T) { assert.True(t, session.ExpiresAt.After(time.Now())) } -func TestLogin_Bad(t *testing.T) { +func TestAuth_Login_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("nancy", "correct-password") @@ -383,7 +383,7 @@ func TestLogin_Bad(t *testing.T) { assert.Contains(t, err.Error(), "invalid password") } -func TestLogin_Ugly(t *testing.T) { +func TestAuth_Login_Ugly(t *testing.T) { a, _ := newTestAuth() // Login for non-existent user @@ -394,7 +394,7 @@ func TestLogin_Ugly(t *testing.T) { // --- WriteChallengeFile / ReadResponseFile (Air-Gapped) --- -func TestAirGappedFlow_Good(t *testing.T) { +func TestAuth_AirGappedFlow_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("oscar", "airgap-pass") @@ -439,7 +439,7 @@ func TestAirGappedFlow_Good(t *testing.T) { assert.Equal(t, userID, session.UserID) } -func TestWriteChallengeFile_Bad(t *testing.T) { +func TestAuth_WriteChallengeFile_Bad(t *testing.T) { a, _ := newTestAuth() // Challenge for non-existent user @@ -447,7 +447,7 @@ func TestWriteChallengeFile_Bad(t *testing.T) { assert.Error(t, err) } -func TestReadResponseFile_Bad(t *testing.T) { +func TestAuth_ReadResponseFile_Bad(t *testing.T) { a, _ := newTestAuth() // Response file does not exist @@ -455,7 +455,7 @@ func TestReadResponseFile_Bad(t *testing.T) { assert.Error(t, err) } -func TestReadResponseFile_Ugly(t *testing.T) { +func TestAuth_ReadResponseFile_Ugly(t *testing.T) { a, m := newTestAuth() _, err := a.Register("peggy", "pass") @@ -477,13 +477,13 @@ func TestReadResponseFile_Ugly(t *testing.T) { // --- Options --- -func TestWithChallengeTTL_Good(t *testing.T) { +func TestAuth_WithChallengeTTL_Good(t *testing.T) { ttl := 30 * time.Second a, _ := newTestAuth(WithChallengeTTL(ttl)) assert.Equal(t, ttl, a.challengeTTL) } -func TestWithSessionTTL_Good(t *testing.T) { +func TestAuth_WithSessionTTL_Good(t *testing.T) { ttl := 2 * time.Hour a, _ := newTestAuth(WithSessionTTL(ttl)) assert.Equal(t, ttl, a.sessionTTL) @@ -491,7 +491,7 @@ func TestWithSessionTTL_Good(t *testing.T) { // --- Full Round-Trip (Online Flow) --- -func TestFullRoundTrip_Good(t *testing.T) { +func TestAuth_FullRoundTrip_Good(t *testing.T) { a, m := newTestAuth() // 1. Register @@ -541,7 +541,7 @@ func TestFullRoundTrip_Good(t *testing.T) { // --- Concurrent Access --- -func TestConcurrentSessions_Good(t *testing.T) { +func TestAuth_ConcurrentSessions_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("ruth", "pass") @@ -579,9 +579,9 @@ func TestConcurrentSessions_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestConcurrentSessionCreation_Good verifies that 10 goroutines creating +// TestAuth_ConcurrentSessionCreation_Good verifies that 10 goroutines creating // sessions simultaneously do not produce data races or errors. -func TestConcurrentSessionCreation_Good(t *testing.T) { +func TestAuth_ConcurrentSessionCreation_Good(t *testing.T) { a, _ := newTestAuth() // Register 10 distinct users to avoid contention on a single user record @@ -619,9 +619,9 @@ func TestConcurrentSessionCreation_Good(t *testing.T) { } } -// TestSessionTokenUniqueness_Good generates 1000 session tokens and verifies +// TestAuth_SessionTokenUniqueness_Good generates 1000 session tokens and verifies // no collisions without paying the full login hash-verification cost each time. -func TestSessionTokenUniqueness_Good(t *testing.T) { +func TestAuth_SessionTokenUniqueness_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("uniqueness-test", "pass") @@ -645,9 +645,9 @@ func TestSessionTokenUniqueness_Good(t *testing.T) { assert.Len(t, tokens, n, "all 1000 tokens should be unique") } -// TestChallengeExpiryBoundary_Ugly tests validation right at the 5-minute boundary. +// TestAuth_ChallengeExpiryBoundary_Ugly tests validation right at the 5-minute boundary. // The challenge should still be valid just before expiry and rejected after. -func TestChallengeExpiryBoundary_Ugly(t *testing.T) { +func TestAuth_ChallengeExpiryBoundary_Ugly(t *testing.T) { // Use a very short TTL to test the boundary without sleeping 5 minutes ttl := 50 * time.Millisecond a, m := newTestAuth(WithChallengeTTL(ttl)) @@ -691,9 +691,9 @@ func TestChallengeExpiryBoundary_Ugly(t *testing.T) { assert.Contains(t, err.Error(), "challenge expired") } -// TestEmptyPasswordRegistration_Good verifies that empty password registration works. +// TestAuth_EmptyPasswordRegistration_Good verifies that empty password registration works. // PGP key is generated unencrypted in this case. -func TestEmptyPasswordRegistration_Good(t *testing.T) { +func TestAuth_EmptyPasswordRegistration_Good(t *testing.T) { a, m := newTestAuth() user, err := a.Register("no-password-user", "") @@ -730,8 +730,8 @@ func TestEmptyPasswordRegistration_Good(t *testing.T) { assert.NotNil(t, crSession) } -// TestVeryLongUsername_Ugly verifies behaviour with a 10K character username. -func TestVeryLongUsername_Ugly(t *testing.T) { +// TestAuth_VeryLongUsername_Ugly verifies behaviour with a 10K character username. +func TestAuth_VeryLongUsername_Ugly(t *testing.T) { a, _ := newTestAuth() longName := core.NewBuilder() @@ -753,8 +753,8 @@ func TestVeryLongUsername_Ugly(t *testing.T) { assert.NotNil(t, session) } -// TestUnicodeUsernamePassword_Good verifies registration and login with Unicode characters. -func TestUnicodeUsernamePassword_Good(t *testing.T) { +// TestAuth_UnicodeUsernamePassword_Good verifies registration and login with Unicode characters. +func TestAuth_UnicodeUsernamePassword_Good(t *testing.T) { a, _ := newTestAuth() // Japanese + emoji + Chinese + Arabic @@ -777,9 +777,9 @@ func TestUnicodeUsernamePassword_Good(t *testing.T) { assert.Error(t, err) } -// TestAirGappedRoundTrip_Good tests the full air-gapped flow: +// TestAuth_AirGappedRoundTrip_Good tests the full air-gapped flow: // WriteChallengeFile -> client signs offline -> ReadResponseFile -func TestAirGappedRoundTrip_Good(t *testing.T) { +func TestAuth_AirGappedRoundTrip_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("airgap-roundtrip", "courier-pass") @@ -832,8 +832,8 @@ func TestAirGappedRoundTrip_Good(t *testing.T) { assert.Equal(t, session.Token, validated.Token) } -// TestRefreshExpiredSession_Bad verifies that refreshing an already-expired session fails. -func TestRefreshExpiredSession_Bad(t *testing.T) { +// TestAuth_RefreshExpiredSession_Bad verifies that refreshing an already-expired session fails. +func TestAuth_RefreshExpiredSession_Bad(t *testing.T) { a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond)) _, err := a.Register("expired-refresh", "pass") @@ -859,8 +859,8 @@ func TestRefreshExpiredSession_Bad(t *testing.T) { // --- Phase 2: Password Hash Migration --- -// TestRegisterArgon2id_Good verifies that new registrations use Argon2id format. -func TestRegisterArgon2id_Good(t *testing.T) { +// TestAuth_RegisterArgon2id_Good verifies that new registrations use Argon2id format. +func TestAuth_RegisterArgon2id_Good(t *testing.T) { a, m := newTestAuth() user, err := a.Register("argon2-user", "strong-pass") @@ -881,8 +881,8 @@ func TestRegisterArgon2id_Good(t *testing.T) { assert.True(t, core.HasPrefix(user.PasswordHash, "$argon2id$")) } -// TestLoginArgon2id_Good verifies login works with Argon2id hashed password. -func TestLoginArgon2id_Good(t *testing.T) { +// TestAuth_LoginArgon2id_Good verifies login works with Argon2id hashed password. +func TestAuth_LoginArgon2id_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("login-argon2", "my-password") @@ -895,8 +895,8 @@ func TestLoginArgon2id_Good(t *testing.T) { assert.NotEmpty(t, session.Token) } -// TestLoginArgon2id_Bad verifies wrong password fails with Argon2id hash. -func TestLoginArgon2id_Bad(t *testing.T) { +// TestAuth_LoginArgon2id_Bad verifies wrong password fails with Argon2id hash. +func TestAuth_LoginArgon2id_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("login-argon2-bad", "correct") @@ -908,9 +908,9 @@ func TestLoginArgon2id_Bad(t *testing.T) { assert.Contains(t, err.Error(), "invalid password") } -// TestLegacyLTHNMigration_Good verifies that a user registered with the legacy +// TestAuth_LegacyLTHNMigration_Good verifies that a user registered with the legacy // LTHN hash format is transparently migrated to Argon2id on successful login. -func TestLegacyLTHNMigration_Good(t *testing.T) { +func TestAuth_LegacyLTHNMigration_Good(t *testing.T) { m := io.NewMockMedium() a := New(m) @@ -950,8 +950,8 @@ func TestLegacyLTHNMigration_Good(t *testing.T) { assert.NotEmpty(t, session2.Token) } -// TestLegacyLTHNLogin_Bad verifies wrong password fails for legacy LTHN users. -func TestLegacyLTHNLogin_Bad(t *testing.T) { +// TestAuth_LegacyLTHNLogin_Bad verifies wrong password fails for legacy LTHN users. +func TestAuth_LegacyLTHNLogin_Bad(t *testing.T) { m := io.NewMockMedium() a := New(m) @@ -976,9 +976,9 @@ func TestLegacyLTHNLogin_Bad(t *testing.T) { // --- Phase 2: Key Rotation --- -// TestRotateKeyPair_Good verifies the full key rotation flow: +// TestAuth_RotateKeyPair_Good verifies the full key rotation flow: // register -> login -> rotate -> verify old key can't decrypt -> verify new key works -> sessions invalidated. -func TestRotateKeyPair_Good(t *testing.T) { +func TestAuth_RotateKeyPair_Good(t *testing.T) { a, m := newTestAuth() // Register and login @@ -1032,8 +1032,8 @@ func TestRotateKeyPair_Good(t *testing.T) { assert.True(t, core.HasPrefix(meta.PasswordHash, "$argon2id$")) } -// TestRotateKeyPair_Bad verifies that rotation fails with wrong old password. -func TestRotateKeyPair_Bad(t *testing.T) { +// TestAuth_RotateKeyPair_Bad verifies that rotation fails with wrong old password. +func TestAuth_RotateKeyPair_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("rotate-bad", "correct-pass") @@ -1046,8 +1046,8 @@ func TestRotateKeyPair_Bad(t *testing.T) { assert.Contains(t, err.Error(), "failed to decrypt metadata") } -// TestRotateKeyPair_Ugly verifies rotation for non-existent user. -func TestRotateKeyPair_Ugly(t *testing.T) { +// TestAuth_RotateKeyPair_Ugly verifies rotation for non-existent user. +func TestAuth_RotateKeyPair_Ugly(t *testing.T) { a, _ := newTestAuth() _, err := a.RotateKeyPair("nonexistent-user-id", "old", "new") @@ -1055,9 +1055,9 @@ func TestRotateKeyPair_Ugly(t *testing.T) { assert.Contains(t, err.Error(), "user not found") } -// TestRotateKeyPair_OldKeyCannotDecrypt_Good verifies old private key +// TestAuth_RotateKeyPair_OldKeyCannotDecrypt_Good verifies old private key // cannot decrypt metadata after rotation. -func TestRotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) { +func TestAuth_RotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("rotate-crypto", "pass-a") @@ -1081,9 +1081,9 @@ func TestRotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) { // --- Phase 2: Key Revocation --- -// TestRevokeKey_Good verifies the full revocation flow: +// TestAuth_RevokeKey_Good verifies the full revocation flow: // register -> login -> revoke -> login fails -> challenge fails -> sessions invalidated. -func TestRevokeKey_Good(t *testing.T) { +func TestAuth_RevokeKey_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("revoke-user", "pass") @@ -1131,8 +1131,8 @@ func TestRevokeKey_Good(t *testing.T) { assert.Error(t, err) } -// TestRevokeKey_Bad verifies revocation fails with wrong password. -func TestRevokeKey_Bad(t *testing.T) { +// TestAuth_RevokeKey_Bad verifies revocation fails with wrong password. +func TestAuth_RevokeKey_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("revoke-bad", "correct") @@ -1147,8 +1147,8 @@ func TestRevokeKey_Bad(t *testing.T) { assert.False(t, a.IsRevoked(userID)) } -// TestRevokeKey_Ugly verifies revocation for non-existent user. -func TestRevokeKey_Ugly(t *testing.T) { +// TestAuth_RevokeKey_Ugly verifies revocation for non-existent user. +func TestAuth_RevokeKey_Ugly(t *testing.T) { a, _ := newTestAuth() err := a.RevokeKey("nonexistent-user-id", "pass", "reason") @@ -1156,9 +1156,9 @@ func TestRevokeKey_Ugly(t *testing.T) { assert.Contains(t, err.Error(), "user not found") } -// TestIsRevoked_Placeholder_Good verifies that the legacy placeholder is not +// TestAuth_IsRevoked_Placeholder_Good verifies that the legacy placeholder is not // treated as a valid revocation. -func TestIsRevoked_Placeholder_Good(t *testing.T) { +func TestAuth_IsRevoked_Placeholder_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("placeholder-user", "pass") @@ -1174,16 +1174,16 @@ func TestIsRevoked_Placeholder_Good(t *testing.T) { assert.False(t, a.IsRevoked(userID)) } -// TestIsRevoked_NoRevFile_Good verifies that a missing .rev file returns false. -func TestIsRevoked_NoRevFile_Good(t *testing.T) { +// TestAuth_IsRevoked_NoRevFile_Good verifies that a missing .rev file returns false. +func TestAuth_IsRevoked_NoRevFile_Good(t *testing.T) { a, _ := newTestAuth() assert.False(t, a.IsRevoked("completely-nonexistent")) } -// TestRevokeKey_LegacyUser_Good verifies revocation works for a legacy user +// TestAuth_RevokeKey_LegacyUser_Good verifies revocation works for a legacy user // with only a .lthn hash file (no .hash file). -func TestRevokeKey_LegacyUser_Good(t *testing.T) { +func TestAuth_RevokeKey_LegacyUser_Good(t *testing.T) { m := io.NewMockMedium() a := New(m) diff --git a/auth/hardware.go b/auth/hardware.go index c5017fd..2af5ac7 100644 --- a/auth/hardware.go +++ b/auth/hardware.go @@ -17,6 +17,7 @@ package auth // similar tamper-resistant devices. // // All methods must be safe for concurrent use. +// Usage: implement HardwareKey and pass it to WithHardwareKey(...) to wire hardware-backed auth into New(...). type HardwareKey interface { // Sign produces a cryptographic signature over the given data using the // hardware-stored private key. The signature format depends on the @@ -44,6 +45,7 @@ type HardwareKey interface { // // This is a forward-looking option — integration points are documented in // auth.go but not yet wired up. +// Usage: pass WithHardwareKey(...) into New(...) to enable a HardwareKey implementation. func WithHardwareKey(hk HardwareKey) Option { return func(a *Authenticator) { a.hardwareKey = hk diff --git a/auth/session_store.go b/auth/session_store.go index f38f266..aed4ffc 100644 --- a/auth/session_store.go +++ b/auth/session_store.go @@ -9,9 +9,11 @@ import ( ) // ErrSessionNotFound is returned when a session token is not found. +// Usage: compare returned errors against ErrSessionNotFound when branching on failures. var ErrSessionNotFound = coreerr.E("auth", "session not found", nil) // SessionStore abstracts session persistence. +// Usage: use SessionStore with the other exported helpers in this package. type SessionStore interface { Get(token string) (*Session, error) Set(session *Session) error @@ -21,12 +23,14 @@ type SessionStore interface { } // MemorySessionStore is an in-memory SessionStore backed by a map. +// Usage: use MemorySessionStore with the other exported helpers in this package. type MemorySessionStore struct { mu sync.RWMutex sessions map[string]*Session } // NewMemorySessionStore creates a new in-memory session store. +// Usage: call NewMemorySessionStore(...) to create a ready-to-use value. func NewMemorySessionStore() *MemorySessionStore { return &MemorySessionStore{ sessions: make(map[string]*Session), @@ -34,6 +38,7 @@ func NewMemorySessionStore() *MemorySessionStore { } // Get retrieves a session by token. +// Usage: call Get(...) during the package's normal workflow. func (m *MemorySessionStore) Get(token string) (*Session, error) { m.mu.RLock() defer m.mu.RUnlock() @@ -49,6 +54,7 @@ func (m *MemorySessionStore) Get(token string) (*Session, error) { } // Set stores a session, keyed by its token. +// Usage: call Set(...) during the package's normal workflow. func (m *MemorySessionStore) Set(session *Session) error { m.mu.Lock() defer m.mu.Unlock() @@ -60,6 +66,7 @@ func (m *MemorySessionStore) Set(session *Session) error { } // Delete removes a session by token. +// Usage: call Delete(...) during the package's normal workflow. func (m *MemorySessionStore) Delete(token string) error { m.mu.Lock() defer m.mu.Unlock() @@ -73,6 +80,7 @@ func (m *MemorySessionStore) Delete(token string) error { } // DeleteByUser removes all sessions belonging to the given user. +// Usage: call DeleteByUser(...) during the package's normal workflow. func (m *MemorySessionStore) DeleteByUser(userID string) error { m.mu.Lock() defer m.mu.Unlock() @@ -84,6 +92,7 @@ func (m *MemorySessionStore) DeleteByUser(userID string) error { } // Cleanup removes all expired sessions and returns the count removed. +// Usage: call Cleanup(...) during the package's normal workflow. func (m *MemorySessionStore) Cleanup() (int, error) { m.mu.Lock() defer m.mu.Unlock() diff --git a/auth/session_store_sqlite.go b/auth/session_store_sqlite.go index 1d61ff2..73993d5 100644 --- a/auth/session_store_sqlite.go +++ b/auth/session_store_sqlite.go @@ -12,6 +12,7 @@ const sessionGroup = "sessions" // SQLiteSessionStore is a SessionStore backed by core/store (SQLite KV). // A mutex serialises all operations because SQLite is single-writer. +// Usage: use SQLiteSessionStore with the other exported helpers in this package. type SQLiteSessionStore struct { mu sync.Mutex store *store.Store @@ -19,6 +20,7 @@ type SQLiteSessionStore struct { // NewSQLiteSessionStore creates a new SQLite-backed session store. // Use ":memory:" for testing or a file path for persistent storage. +// Usage: call NewSQLiteSessionStore(...) to create a ready-to-use value. func NewSQLiteSessionStore(dbPath string) (*SQLiteSessionStore, error) { s, err := store.New(dbPath) if err != nil { @@ -28,6 +30,7 @@ func NewSQLiteSessionStore(dbPath string) (*SQLiteSessionStore, error) { } // Get retrieves a session by token from SQLite. +// Usage: call Get(...) during the package's normal workflow. func (s *SQLiteSessionStore) Get(token string) (*Session, error) { s.mu.Lock() defer s.mu.Unlock() @@ -50,6 +53,7 @@ func (s *SQLiteSessionStore) Get(token string) (*Session, error) { } // Set stores a session in SQLite, keyed by its token. +// Usage: call Set(...) during the package's normal workflow. func (s *SQLiteSessionStore) Set(session *Session) error { s.mu.Lock() defer s.mu.Unlock() @@ -63,6 +67,7 @@ func (s *SQLiteSessionStore) Set(session *Session) error { } // Delete removes a session by token from SQLite. +// Usage: call Delete(...) during the package's normal workflow. func (s *SQLiteSessionStore) Delete(token string) error { s.mu.Lock() defer s.mu.Unlock() @@ -79,6 +84,7 @@ func (s *SQLiteSessionStore) Delete(token string) error { } // DeleteByUser removes all sessions belonging to the given user. +// Usage: call DeleteByUser(...) during the package's normal workflow. func (s *SQLiteSessionStore) DeleteByUser(userID string) error { s.mu.Lock() defer s.mu.Unlock() @@ -104,6 +110,7 @@ func (s *SQLiteSessionStore) DeleteByUser(userID string) error { } // Cleanup removes all expired sessions and returns the count removed. +// Usage: call Cleanup(...) during the package's normal workflow. func (s *SQLiteSessionStore) Cleanup() (int, error) { s.mu.Lock() defer s.mu.Unlock() @@ -132,6 +139,7 @@ func (s *SQLiteSessionStore) Cleanup() (int, error) { } // Close closes the underlying SQLite store. +// Usage: call Close(...) during the package's normal workflow. func (s *SQLiteSessionStore) Close() error { s.mu.Lock() defer s.mu.Unlock() diff --git a/auth/session_store_test.go b/auth/session_store_test.go index 977e159..896bdbf 100644 --- a/auth/session_store_test.go +++ b/auth/session_store_test.go @@ -16,7 +16,7 @@ import ( // --- MemorySessionStore --- -func TestMemorySessionStore_GetSetDelete_Good(t *testing.T) { +func TestSessionStore_MemorySessionStore_GetSetDelete_Good(t *testing.T) { store := NewMemorySessionStore() session := &Session{ @@ -44,21 +44,21 @@ func TestMemorySessionStore_GetSetDelete_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestMemorySessionStore_GetNotFound_Bad(t *testing.T) { +func TestSessionStore_MemorySessionStore_GetNotFound_Bad(t *testing.T) { store := NewMemorySessionStore() _, err := store.Get("nonexistent-token") assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestMemorySessionStore_DeleteNotFound_Bad(t *testing.T) { +func TestSessionStore_MemorySessionStore_DeleteNotFound_Bad(t *testing.T) { store := NewMemorySessionStore() err := store.Delete("nonexistent-token") assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) { +func TestSessionStore_MemorySessionStore_DeleteByUser_Good(t *testing.T) { store := NewMemorySessionStore() // Create sessions for two users @@ -94,7 +94,7 @@ func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) { assert.Equal(t, "user-b", got.UserID) } -func TestMemorySessionStore_Cleanup_Good(t *testing.T) { +func TestSessionStore_MemorySessionStore_Cleanup_Good(t *testing.T) { store := NewMemorySessionStore() // Create expired and valid sessions @@ -134,7 +134,7 @@ func TestMemorySessionStore_Cleanup_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestMemorySessionStore_Concurrent_Good(t *testing.T) { +func TestSessionStore_MemorySessionStore_Concurrent_Good(t *testing.T) { store := NewMemorySessionStore() const n = 20 @@ -164,7 +164,7 @@ func TestMemorySessionStore_Concurrent_Good(t *testing.T) { // --- SQLiteSessionStore --- -func TestSQLiteSessionStore_GetSetDelete_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_GetSetDelete_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -194,7 +194,7 @@ func TestSQLiteSessionStore_GetSetDelete_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSQLiteSessionStore_GetNotFound_Bad(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_GetNotFound_Bad(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -203,7 +203,7 @@ func TestSQLiteSessionStore_GetNotFound_Bad(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -212,7 +212,7 @@ func TestSQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_DeleteByUser_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -250,7 +250,7 @@ func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) { assert.Equal(t, "user-b", got.UserID) } -func TestSQLiteSessionStore_Cleanup_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_Cleanup_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -292,7 +292,7 @@ func TestSQLiteSessionStore_Cleanup_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSQLiteSessionStore_Persistence_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_Persistence_Good(t *testing.T) { dir := t.TempDir() dbPath := core.Path(dir, "sessions.db") @@ -323,7 +323,7 @@ func TestSQLiteSessionStore_Persistence_Good(t *testing.T) { assert.Equal(t, "persist-token", got.Token) } -func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_Concurrent_Good(t *testing.T) { // Use a temp file — :memory: SQLite has concurrency limitations dbPath := core.Path(t.TempDir(), "concurrent.db") store, err := NewSQLiteSessionStore(dbPath) @@ -359,7 +359,7 @@ func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) { // --- Authenticator with SessionStore --- -func TestAuthenticator_WithSessionStore_Good(t *testing.T) { +func TestSessionStore_Authenticator_WithSessionStore_Good(t *testing.T) { sqliteStore, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer sqliteStore.Close() @@ -398,7 +398,7 @@ func TestAuthenticator_WithSessionStore_Good(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestAuthenticator_DefaultStore_Good(t *testing.T) { +func TestSessionStore_Authenticator_DefaultStore_Good(t *testing.T) { m := io.NewMockMedium() a := New(m) @@ -407,7 +407,7 @@ func TestAuthenticator_DefaultStore_Good(t *testing.T) { assert.True(t, ok, "default store should be MemorySessionStore") } -func TestAuthenticator_StartCleanup_Good(t *testing.T) { +func TestSessionStore_Authenticator_StartCleanup_Good(t *testing.T) { m := io.NewMockMedium() a := New(m, WithSessionTTL(1*time.Millisecond)) @@ -436,7 +436,7 @@ func TestAuthenticator_StartCleanup_Good(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestAuthenticator_StartCleanup_CancelStops_Good(t *testing.T) { +func TestSessionStore_Authenticator_StartCleanup_CancelStops_Good(t *testing.T) { m := io.NewMockMedium() a := New(m) @@ -448,7 +448,7 @@ func TestAuthenticator_StartCleanup_CancelStops_Good(t *testing.T) { time.Sleep(50 * time.Millisecond) } -func TestSQLiteSessionStore_UpdateExisting_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_UpdateExisting_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -476,7 +476,7 @@ func TestSQLiteSessionStore_UpdateExisting_Good(t *testing.T) { "updated session should have later expiry") } -func TestSQLiteSessionStore_TempFile_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_TempFile_Good(t *testing.T) { // Verify we can use a real temp file (not :memory:) tmpFile := core.Path(t.TempDir(), "go-crypt-test-session-store.db") diff --git a/cmd/crypt/cmd.go b/cmd/crypt/cmd.go index 66101cd..c544622 100644 --- a/cmd/crypt/cmd.go +++ b/cmd/crypt/cmd.go @@ -7,6 +7,7 @@ func init() { } // AddCryptCommands registers the 'crypt' command group and all subcommands. +// Usage: call AddCryptCommands(...) during the package's normal workflow. func AddCryptCommands(root *cli.Command) { cryptCmd := &cli.Command{ Use: "crypt", diff --git a/cmd/testcmd/cmd_main.go b/cmd/testcmd/cmd_main.go index 17892a1..0d2950b 100644 --- a/cmd/testcmd/cmd_main.go +++ b/cmd/testcmd/cmd_main.go @@ -56,6 +56,7 @@ func initTestFlags() { } // AddTestCommands registers the 'test' command and all subcommands. +// Usage: call AddTestCommands(...) during the package's normal workflow. func AddTestCommands(root *cli.Command) { initTestFlags() root.AddCommand(testCmd) diff --git a/cmd/testcmd/output_test.go b/cmd/testcmd/output_test.go index 381befd..80ed8b0 100644 --- a/cmd/testcmd/output_test.go +++ b/cmd/testcmd/output_test.go @@ -6,19 +6,19 @@ import ( "github.com/stretchr/testify/assert" ) -func TestShortenPackageName_Good(t *testing.T) { +func TestOutput_ShortenPackageName_Good(t *testing.T) { assert.Equal(t, "pkg/foo", shortenPackageName("dappco.re/go/core/pkg/foo")) assert.Equal(t, "cli-php", shortenPackageName("example.com/org/cli-php")) assert.Equal(t, "bar", shortenPackageName("github.com/other/bar")) } -func TestFormatCoverage_Good(t *testing.T) { +func TestOutput_FormatCoverage_Good(t *testing.T) { assert.Contains(t, formatCoverage(85.0), "85.0%") assert.Contains(t, formatCoverage(65.0), "65.0%") assert.Contains(t, formatCoverage(25.0), "25.0%") } -func TestParseTestOutput_Good(t *testing.T) { +func TestOutput_ParseTestOutput_Good(t *testing.T) { output := `ok dappco.re/go/core/pkg/foo 0.100s coverage: 50.0% of statements FAIL dappco.re/go/core/pkg/bar ? dappco.re/go/core/pkg/baz [no test files] @@ -33,7 +33,7 @@ FAIL dappco.re/go/core/pkg/bar assert.Equal(t, 50.0, results.packages[0].coverage) } -func TestPrintCoverageSummary_Good_LongPackageNames(t *testing.T) { +func TestOutput_PrintCoverageSummary_Good_LongPackageNames(t *testing.T) { // This tests the bug fix for long package names causing negative Repeat count results := testResults{ packages: []packageCoverage{ diff --git a/crypt/chachapoly/chachapoly.go b/crypt/chachapoly/chachapoly.go index 733feaa..af66835 100644 --- a/crypt/chachapoly/chachapoly.go +++ b/crypt/chachapoly/chachapoly.go @@ -11,6 +11,7 @@ import ( ) // Encrypt encrypts data using ChaCha20-Poly1305. +// Usage: call Encrypt(...) during the package's normal workflow. func Encrypt(plaintext []byte, key []byte) ([]byte, error) { aead, err := chacha20poly1305.NewX(key) if err != nil { @@ -26,6 +27,7 @@ func Encrypt(plaintext []byte, key []byte) ([]byte, error) { } // Decrypt decrypts data using ChaCha20-Poly1305. +// Usage: call Decrypt(...) during the package's normal workflow. func Decrypt(ciphertext []byte, key []byte) ([]byte, error) { aead, err := chacha20poly1305.NewX(key) if err != nil { diff --git a/crypt/chachapoly/chachapoly_test.go b/crypt/chachapoly/chachapoly_test.go index 8e2c548..2c281db 100644 --- a/crypt/chachapoly/chachapoly_test.go +++ b/crypt/chachapoly/chachapoly_test.go @@ -15,7 +15,7 @@ func (r *mockReader) Read(p []byte) (n int, err error) { return 0, core.NewError("read error") } -func TestEncryptDecrypt_Good(t *testing.T) { +func TestChachapoly_EncryptDecrypt_Good(t *testing.T) { key := make([]byte, 32) for i := range key { key[i] = 1 @@ -31,14 +31,14 @@ func TestEncryptDecrypt_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestEncrypt_Bad_InvalidKeySize(t *testing.T) { +func TestChachapoly_Encrypt_Bad_InvalidKeySize(t *testing.T) { key := make([]byte, 16) // Wrong size plaintext := []byte("test") _, err := Encrypt(plaintext, key) assert.Error(t, err) } -func TestDecrypt_Bad_WrongKey(t *testing.T) { +func TestChachapoly_Decrypt_Bad_WrongKey(t *testing.T) { key1 := make([]byte, 32) key2 := make([]byte, 32) key2[0] = 1 // Different key @@ -51,7 +51,7 @@ func TestDecrypt_Bad_WrongKey(t *testing.T) { assert.Error(t, err) // Should fail authentication } -func TestDecrypt_Bad_TamperedCiphertext(t *testing.T) { +func TestChachapoly_Decrypt_Bad_TamperedCiphertext(t *testing.T) { key := make([]byte, 32) plaintext := []byte("secret") ciphertext, err := Encrypt(plaintext, key) @@ -64,7 +64,7 @@ func TestDecrypt_Bad_TamperedCiphertext(t *testing.T) { assert.Error(t, err) } -func TestEncrypt_Good_EmptyPlaintext(t *testing.T) { +func TestChachapoly_Encrypt_Good_EmptyPlaintext(t *testing.T) { key := make([]byte, 32) plaintext := []byte("") ciphertext, err := Encrypt(plaintext, key) @@ -76,7 +76,7 @@ func TestEncrypt_Good_EmptyPlaintext(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestDecrypt_Bad_ShortCiphertext(t *testing.T) { +func TestChachapoly_Decrypt_Bad_ShortCiphertext(t *testing.T) { key := make([]byte, 32) shortCiphertext := []byte("short") @@ -85,7 +85,7 @@ func TestDecrypt_Bad_ShortCiphertext(t *testing.T) { assert.Contains(t, err.Error(), "too short") } -func TestCiphertextDiffersFromPlaintext_Good(t *testing.T) { +func TestChachapoly_CiphertextDiffersFromPlaintext_Good(t *testing.T) { key := make([]byte, 32) plaintext := []byte("Hello, world!") ciphertext, err := Encrypt(plaintext, key) @@ -93,7 +93,7 @@ func TestCiphertextDiffersFromPlaintext_Good(t *testing.T) { assert.NotEqual(t, plaintext, ciphertext) } -func TestEncrypt_Bad_NonceError(t *testing.T) { +func TestChachapoly_Encrypt_Bad_NonceError(t *testing.T) { key := make([]byte, 32) plaintext := []byte("test") @@ -106,7 +106,7 @@ func TestEncrypt_Bad_NonceError(t *testing.T) { assert.Error(t, err) } -func TestDecrypt_Bad_InvalidKeySize(t *testing.T) { +func TestChachapoly_Decrypt_Bad_InvalidKeySize(t *testing.T) { key := make([]byte, 16) // Wrong size ciphertext := []byte("test") _, err := Decrypt(ciphertext, key) diff --git a/crypt/checksum.go b/crypt/checksum.go index 80b8af5..621e3ff 100644 --- a/crypt/checksum.go +++ b/crypt/checksum.go @@ -11,6 +11,7 @@ import ( ) // SHA256File computes the SHA-256 checksum of a file and returns it as a hex string. +// Usage: call SHA256File(...) during the package's normal workflow. func SHA256File(path string) (string, error) { openResult := (&core.Fs{}).New("/").Open(path) if !openResult.OK { @@ -29,6 +30,7 @@ func SHA256File(path string) (string, error) { } // SHA512File computes the SHA-512 checksum of a file and returns it as a hex string. +// Usage: call SHA512File(...) during the package's normal workflow. func SHA512File(path string) (string, error) { openResult := (&core.Fs{}).New("/").Open(path) if !openResult.OK { @@ -47,12 +49,14 @@ func SHA512File(path string) (string, error) { } // SHA256Sum computes the SHA-256 checksum of data and returns it as a hex string. +// Usage: call SHA256Sum(...) during the package's normal workflow. func SHA256Sum(data []byte) string { h := sha256.Sum256(data) return hex.EncodeToString(h[:]) } // SHA512Sum computes the SHA-512 checksum of data and returns it as a hex string. +// Usage: call SHA512Sum(...) during the package's normal workflow. func SHA512Sum(data []byte) string { h := sha512.Sum512(data) return hex.EncodeToString(h[:]) diff --git a/crypt/checksum_test.go b/crypt/checksum_test.go index 03f92bb..4b4a7df 100644 --- a/crypt/checksum_test.go +++ b/crypt/checksum_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestSHA256Sum_Good(t *testing.T) { +func TestChecksum_SHA256Sum_Good(t *testing.T) { data := []byte("hello") expected := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" @@ -16,7 +16,7 @@ func TestSHA256Sum_Good(t *testing.T) { assert.Equal(t, expected, result) } -func TestSHA512Sum_Good(t *testing.T) { +func TestChecksum_SHA512Sum_Good(t *testing.T) { data := []byte("hello") expected := "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043" @@ -26,8 +26,8 @@ func TestSHA512Sum_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestSHA256FileEmpty_Good verifies checksum of an empty file. -func TestSHA256FileEmpty_Good(t *testing.T) { +// TestChecksum_SHA256FileEmpty_Good verifies checksum of an empty file. +func TestChecksum_SHA256FileEmpty_Good(t *testing.T) { tmpDir := t.TempDir() emptyFile := core.Path(tmpDir, "empty.bin") writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644) @@ -39,8 +39,8 @@ func TestSHA256FileEmpty_Good(t *testing.T) { assert.Equal(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hash) } -// TestSHA512FileEmpty_Good verifies SHA-512 checksum of an empty file. -func TestSHA512FileEmpty_Good(t *testing.T) { +// TestChecksum_SHA512FileEmpty_Good verifies SHA-512 checksum of an empty file. +func TestChecksum_SHA512FileEmpty_Good(t *testing.T) { tmpDir := t.TempDir() emptyFile := core.Path(tmpDir, "empty.bin") writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644) @@ -51,22 +51,22 @@ func TestSHA512FileEmpty_Good(t *testing.T) { assert.Equal(t, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", hash) } -// TestSHA256FileNonExistent_Bad verifies error on non-existent file. -func TestSHA256FileNonExistent_Bad(t *testing.T) { +// TestChecksum_SHA256FileNonExistent_Bad verifies error on non-existent file. +func TestChecksum_SHA256FileNonExistent_Bad(t *testing.T) { _, err := SHA256File("/nonexistent/path/to/file.bin") assert.Error(t, err) assert.Contains(t, err.Error(), "failed to open file") } -// TestSHA512FileNonExistent_Bad verifies error on non-existent file. -func TestSHA512FileNonExistent_Bad(t *testing.T) { +// TestChecksum_SHA512FileNonExistent_Bad verifies error on non-existent file. +func TestChecksum_SHA512FileNonExistent_Bad(t *testing.T) { _, err := SHA512File("/nonexistent/path/to/file.bin") assert.Error(t, err) assert.Contains(t, err.Error(), "failed to open file") } -// TestSHA256FileWithContent_Good verifies checksum of a file with known content. -func TestSHA256FileWithContent_Good(t *testing.T) { +// TestChecksum_SHA256FileWithContent_Good verifies checksum of a file with known content. +func TestChecksum_SHA256FileWithContent_Good(t *testing.T) { tmpDir := t.TempDir() testFile := core.Path(tmpDir, "test.txt") writeResult := (&core.Fs{}).New("/").WriteMode(testFile, "hello", 0o644) diff --git a/crypt/crypt.go b/crypt/crypt.go index df18f2f..5cf2a32 100644 --- a/crypt/crypt.go +++ b/crypt/crypt.go @@ -7,6 +7,7 @@ import ( // Encrypt encrypts data with a passphrase using ChaCha20-Poly1305. // A random salt is generated and prepended to the output. // Format: salt (16 bytes) + nonce (24 bytes) + ciphertext. +// Usage: call Encrypt(...) during the package's normal workflow. func Encrypt(plaintext, passphrase []byte) ([]byte, error) { salt, err := generateSalt(argon2SaltLen) if err != nil { @@ -29,6 +30,7 @@ func Encrypt(plaintext, passphrase []byte) ([]byte, error) { // Decrypt decrypts data encrypted with Encrypt. // Expects format: salt (16 bytes) + nonce (24 bytes) + ciphertext. +// Usage: call Decrypt(...) during the package's normal workflow. func Decrypt(ciphertext, passphrase []byte) ([]byte, error) { if len(ciphertext) < argon2SaltLen { return nil, coreerr.E("crypt.Decrypt", "ciphertext too short", nil) @@ -50,6 +52,7 @@ func Decrypt(ciphertext, passphrase []byte) ([]byte, error) { // EncryptAES encrypts data using AES-256-GCM with a passphrase. // A random salt is generated and prepended to the output. // Format: salt (16 bytes) + nonce (12 bytes) + ciphertext. +// Usage: call EncryptAES(...) during the package's normal workflow. func EncryptAES(plaintext, passphrase []byte) ([]byte, error) { salt, err := generateSalt(argon2SaltLen) if err != nil { @@ -71,6 +74,7 @@ func EncryptAES(plaintext, passphrase []byte) ([]byte, error) { // DecryptAES decrypts data encrypted with EncryptAES. // Expects format: salt (16 bytes) + nonce (12 bytes) + ciphertext. +// Usage: call DecryptAES(...) during the package's normal workflow. func DecryptAES(ciphertext, passphrase []byte) ([]byte, error) { if len(ciphertext) < argon2SaltLen { return nil, coreerr.E("crypt.DecryptAES", "ciphertext too short", nil) diff --git a/crypt/crypt_test.go b/crypt/crypt_test.go index 7d5c1c6..d266e8a 100644 --- a/crypt/crypt_test.go +++ b/crypt/crypt_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestEncryptDecrypt_Good(t *testing.T) { +func TestCrypt_EncryptDecrypt_Good(t *testing.T) { plaintext := []byte("hello, world!") passphrase := []byte("correct-horse-battery-staple") @@ -21,7 +21,7 @@ func TestEncryptDecrypt_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestEncryptDecrypt_Bad(t *testing.T) { +func TestCrypt_EncryptDecrypt_Bad(t *testing.T) { plaintext := []byte("secret data") passphrase := []byte("correct-passphrase") wrongPassphrase := []byte("wrong-passphrase") @@ -33,7 +33,7 @@ func TestEncryptDecrypt_Bad(t *testing.T) { assert.Error(t, err) } -func TestEncryptDecryptAES_Good(t *testing.T) { +func TestCrypt_EncryptDecryptAES_Good(t *testing.T) { plaintext := []byte("hello, AES world!") passphrase := []byte("my-secure-passphrase") @@ -48,8 +48,8 @@ func TestEncryptDecryptAES_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestWrongPassphraseDecrypt_Bad verifies wrong passphrase returns error, not corrupt data. -func TestWrongPassphraseDecrypt_Bad(t *testing.T) { +// TestCrypt_WrongPassphraseDecrypt_Bad verifies wrong passphrase returns error, not corrupt data. +func TestCrypt_WrongPassphraseDecrypt_Bad(t *testing.T) { plaintext := []byte("sensitive payload") passphrase := []byte("correct-passphrase") wrongPassphrase := []byte("wrong-passphrase") @@ -70,8 +70,8 @@ func TestWrongPassphraseDecrypt_Bad(t *testing.T) { assert.Nil(t, decryptedAES, "wrong passphrase must not return partial data (AES)") } -// TestEmptyPlaintextRoundTrip_Good verifies encrypt/decrypt of empty plaintext. -func TestEmptyPlaintextRoundTrip_Good(t *testing.T) { +// TestCrypt_EmptyPlaintextRoundTrip_Good verifies encrypt/decrypt of empty plaintext. +func TestCrypt_EmptyPlaintextRoundTrip_Good(t *testing.T) { passphrase := []byte("test-passphrase") // ChaCha20 @@ -93,8 +93,8 @@ func TestEmptyPlaintextRoundTrip_Good(t *testing.T) { assert.Empty(t, decryptedAES) } -// TestLargePlaintextRoundTrip_Good verifies encrypt/decrypt of a 1MB payload. -func TestLargePlaintextRoundTrip_Good(t *testing.T) { +// TestCrypt_LargePlaintextRoundTrip_Good verifies encrypt/decrypt of a 1MB payload. +func TestCrypt_LargePlaintextRoundTrip_Good(t *testing.T) { passphrase := []byte("large-payload-passphrase") plaintext := bytes.Repeat([]byte("X"), 1024*1024) // 1MB @@ -116,8 +116,8 @@ func TestLargePlaintextRoundTrip_Good(t *testing.T) { assert.Equal(t, plaintext, decryptedAES) } -// TestDecryptCiphertextTooShort_Ugly verifies short ciphertext is rejected. -func TestDecryptCiphertextTooShort_Ugly(t *testing.T) { +// TestCrypt_DecryptCiphertextTooShort_Ugly verifies short ciphertext is rejected. +func TestCrypt_DecryptCiphertextTooShort_Ugly(t *testing.T) { _, err := Decrypt([]byte("short"), []byte("pass")) assert.Error(t, err) assert.Contains(t, err.Error(), "too short") diff --git a/crypt/hash.go b/crypt/hash.go index bd08003..7239f28 100644 --- a/crypt/hash.go +++ b/crypt/hash.go @@ -14,6 +14,7 @@ import ( // HashPassword hashes a password using Argon2id with default parameters. // Returns a string in the format: $argon2id$v=19$m=65536,t=3,p=4$$ +// Usage: call HashPassword(...) during the package's normal workflow. func HashPassword(password string) (string, error) { salt, err := generateSalt(argon2SaltLen) if err != nil { @@ -34,6 +35,7 @@ func HashPassword(password string) (string, error) { // VerifyPassword verifies a password against an Argon2id hash string. // The hash must be in the format produced by HashPassword. +// Usage: call VerifyPassword(...) during the package's normal workflow. func VerifyPassword(password, hash string) (bool, error) { parts := core.Split(hash, "$") if len(parts) != 6 { @@ -116,6 +118,7 @@ func parsePrefixedUint32(input, prefix string) (uint32, error) { // HashBcrypt hashes a password using bcrypt with the given cost. // Cost must be between bcrypt.MinCost and bcrypt.MaxCost. +// Usage: call HashBcrypt(...) during the package's normal workflow. func HashBcrypt(password string, cost int) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), cost) if err != nil { @@ -125,6 +128,7 @@ func HashBcrypt(password string, cost int) (string, error) { } // VerifyBcrypt verifies a password against a bcrypt hash. +// Usage: call VerifyBcrypt(...) during the package's normal workflow. func VerifyBcrypt(password, hash string) (bool, error) { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) if err == bcrypt.ErrMismatchedHashAndPassword { diff --git a/crypt/hash_test.go b/crypt/hash_test.go index ad308a0..8459390 100644 --- a/crypt/hash_test.go +++ b/crypt/hash_test.go @@ -7,7 +7,7 @@ import ( "golang.org/x/crypto/bcrypt" ) -func TestHashPassword_Good(t *testing.T) { +func TestHash_HashPassword_Good(t *testing.T) { password := "my-secure-password" hash, err := HashPassword(password) @@ -20,7 +20,7 @@ func TestHashPassword_Good(t *testing.T) { assert.True(t, match) } -func TestVerifyPassword_Bad(t *testing.T) { +func TestHash_VerifyPassword_Bad(t *testing.T) { password := "my-secure-password" wrongPassword := "wrong-password" @@ -32,7 +32,7 @@ func TestVerifyPassword_Bad(t *testing.T) { assert.False(t, match) } -func TestHashBcrypt_Good(t *testing.T) { +func TestHash_HashBcrypt_Good(t *testing.T) { password := "bcrypt-test-password" hash, err := HashBcrypt(password, bcrypt.DefaultCost) diff --git a/crypt/hmac.go b/crypt/hmac.go index adb80c2..4c94e6d 100644 --- a/crypt/hmac.go +++ b/crypt/hmac.go @@ -8,6 +8,7 @@ import ( ) // HMACSHA256 computes the HMAC-SHA256 of a message using the given key. +// Usage: call HMACSHA256(...) during the package's normal workflow. func HMACSHA256(message, key []byte) []byte { mac := hmac.New(sha256.New, key) mac.Write(message) @@ -15,6 +16,7 @@ func HMACSHA256(message, key []byte) []byte { } // HMACSHA512 computes the HMAC-SHA512 of a message using the given key. +// Usage: call HMACSHA512(...) during the package's normal workflow. func HMACSHA512(message, key []byte) []byte { mac := hmac.New(sha512.New, key) mac.Write(message) @@ -23,6 +25,7 @@ func HMACSHA512(message, key []byte) []byte { // VerifyHMAC verifies an HMAC using constant-time comparison. // hashFunc should be sha256.New, sha512.New, etc. +// Usage: call VerifyHMAC(...) during the package's normal workflow. func VerifyHMAC(message, key, mac []byte, hashFunc func() hash.Hash) bool { expected := hmac.New(hashFunc, key) expected.Write(message) diff --git a/crypt/hmac_test.go b/crypt/hmac_test.go index 31dc474..3f8efe5 100644 --- a/crypt/hmac_test.go +++ b/crypt/hmac_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestHMACSHA256_Good(t *testing.T) { +func TestHMAC_HMACSHA256_Good(t *testing.T) { // RFC 4231 Test Case 2 key := []byte("Jefe") message := []byte("what do ya want for nothing?") @@ -18,7 +18,7 @@ func TestHMACSHA256_Good(t *testing.T) { assert.Equal(t, expected, hex.EncodeToString(mac)) } -func TestVerifyHMAC_Good(t *testing.T) { +func TestHMAC_VerifyHMAC_Good(t *testing.T) { key := []byte("secret-key") message := []byte("test message") @@ -28,7 +28,7 @@ func TestVerifyHMAC_Good(t *testing.T) { assert.True(t, valid) } -func TestVerifyHMAC_Bad(t *testing.T) { +func TestHMAC_VerifyHMAC_Bad(t *testing.T) { key := []byte("secret-key") message := []byte("test message") tampered := []byte("tampered message") diff --git a/crypt/kdf.go b/crypt/kdf.go index c3058e8..3e466a6 100644 --- a/crypt/kdf.go +++ b/crypt/kdf.go @@ -25,12 +25,14 @@ const ( // DeriveKey derives a key from a passphrase using Argon2id with default parameters. // The salt must be argon2SaltLen bytes. keyLen specifies the desired key length. +// Usage: call DeriveKey(...) during the package's normal workflow. func DeriveKey(passphrase, salt []byte, keyLen uint32) []byte { return argon2.IDKey(passphrase, salt, argon2Time, argon2Memory, argon2Parallelism, keyLen) } // DeriveKeyScrypt derives a key from a passphrase using scrypt. // Uses recommended parameters: N=32768, r=8, p=1. +// Usage: call DeriveKeyScrypt(...) during the package's normal workflow. func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error) { key, err := scrypt.Key(passphrase, salt, 32768, 8, 1, keyLen) if err != nil { @@ -42,6 +44,7 @@ func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error) { // HKDF derives a key using HKDF-SHA256. // secret is the input keying material, salt is optional (can be nil), // info is optional context, and keyLen is the desired output length. +// Usage: call HKDF(...) during the package's normal workflow. func HKDF(secret, salt, info []byte, keyLen int) ([]byte, error) { reader := hkdf.New(sha256.New, secret, salt, info) key := make([]byte, keyLen) diff --git a/crypt/kdf_test.go b/crypt/kdf_test.go index 2c2082e..a4ada5b 100644 --- a/crypt/kdf_test.go +++ b/crypt/kdf_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDeriveKey_Good(t *testing.T) { +func TestKDF_DeriveKey_Good(t *testing.T) { passphrase := []byte("test-passphrase") salt := []byte("1234567890123456") // 16 bytes @@ -21,7 +21,7 @@ func TestDeriveKey_Good(t *testing.T) { assert.NotEqual(t, key1, key3) } -func TestDeriveKeyScrypt_Good(t *testing.T) { +func TestKDF_DeriveKeyScrypt_Good(t *testing.T) { passphrase := []byte("test-passphrase") salt := []byte("1234567890123456") @@ -35,7 +35,7 @@ func TestDeriveKeyScrypt_Good(t *testing.T) { assert.Equal(t, key, key2) } -func TestHKDF_Good(t *testing.T) { +func TestKDF_HKDF_Good(t *testing.T) { secret := []byte("input-keying-material") salt := []byte("optional-salt") info := []byte("context-info") @@ -57,8 +57,8 @@ func TestHKDF_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestKeyDerivationDeterminism_Good verifies same passphrase + salt always yields same key. -func TestKeyDerivationDeterminism_Good(t *testing.T) { +// TestKDF_KeyDerivationDeterminism_Good verifies same passphrase + salt always yields same key. +func TestKDF_KeyDerivationDeterminism_Good(t *testing.T) { passphrase := []byte("determinism-test-passphrase") salt := []byte("1234567890123456") // 16 bytes @@ -82,8 +82,8 @@ func TestKeyDerivationDeterminism_Good(t *testing.T) { assert.Equal(t, scryptKey1, scryptKey2, "scrypt must also be deterministic") } -// TestHKDFDifferentInfoStrings_Good verifies different info strings produce different keys. -func TestHKDFDifferentInfoStrings_Good(t *testing.T) { +// TestKDF_HKDFDifferentInfoStrings_Good verifies different info strings produce different keys. +func TestKDF_HKDFDifferentInfoStrings_Good(t *testing.T) { secret := []byte("shared-secret-material") salt := []byte("common-salt") @@ -114,8 +114,8 @@ func TestHKDFDifferentInfoStrings_Good(t *testing.T) { } } -// TestHKDFNilSalt_Good verifies HKDF works with nil salt. -func TestHKDFNilSalt_Good(t *testing.T) { +// TestKDF_HKDFNilSalt_Good verifies HKDF works with nil salt. +func TestKDF_HKDFNilSalt_Good(t *testing.T) { secret := []byte("input-keying-material") info := []byte("context") diff --git a/crypt/lthn/lthn.go b/crypt/lthn/lthn.go index a2404f5..d4d24a9 100644 --- a/crypt/lthn/lthn.go +++ b/crypt/lthn/lthn.go @@ -42,11 +42,13 @@ var keyMap = map[rune]rune{ // SetKeyMap replaces the default character substitution map. // Use this to customize the quasi-salt derivation for specific applications. // Changes affect all subsequent Hash and Verify calls. +// Usage: call SetKeyMap(...) during the package's normal workflow. func SetKeyMap(newKeyMap map[rune]rune) { keyMap = newKeyMap } // GetKeyMap returns the current character substitution map. +// Usage: call GetKeyMap(...) during the package's normal workflow. func GetKeyMap() map[rune]rune { return keyMap } @@ -61,6 +63,7 @@ func GetKeyMap() map[rune]rune { // // The same input always produces the same hash, enabling verification // without storing a separate salt value. +// Usage: call Hash(...) when you need a deterministic content-style digest rather than a password hash. func Hash(input string) string { salt := createSalt(input) hash := sha256.Sum256([]byte(input + salt)) @@ -89,6 +92,7 @@ func createSalt(input string) string { // Verify checks if an input string produces the given hash. // Returns true if Hash(input) equals the provided hash value. // Uses constant-time comparison to prevent timing attacks. +// Usage: call Verify(...) during the package's normal workflow. func Verify(input string, hash string) bool { computed := Hash(input) return subtle.ConstantTimeCompare([]byte(computed), []byte(hash)) == 1 diff --git a/crypt/lthn/lthn_test.go b/crypt/lthn/lthn_test.go index 428c0d2..0291379 100644 --- a/crypt/lthn/lthn_test.go +++ b/crypt/lthn/lthn_test.go @@ -7,36 +7,36 @@ import ( "github.com/stretchr/testify/assert" ) -func TestHash_Good(t *testing.T) { +func TestLTHN_Hash_Good(t *testing.T) { hash := Hash("hello") assert.NotEmpty(t, hash) } -func TestVerify_Good(t *testing.T) { +func TestLTHN_Verify_Good(t *testing.T) { hash := Hash("hello") assert.True(t, Verify("hello", hash)) } -func TestVerify_Bad(t *testing.T) { +func TestLTHN_Verify_Bad(t *testing.T) { hash := Hash("hello") assert.False(t, Verify("world", hash)) } -func TestCreateSalt_Good(t *testing.T) { +func TestLTHN_CreateSalt_Good(t *testing.T) { // "hello" reversed: "olleh" -> "0113h" expected := "0113h" actual := createSalt("hello") assert.Equal(t, expected, actual, "Salt should be correctly created for 'hello'") } -func TestCreateSalt_Bad(t *testing.T) { +func TestLTHN_CreateSalt_Bad(t *testing.T) { // Test with an empty string expected := "" actual := createSalt("") assert.Equal(t, expected, actual, "Salt for an empty string should be empty") } -func TestCreateSalt_Ugly(t *testing.T) { +func TestLTHN_CreateSalt_Ugly(t *testing.T) { // Test with characters not in the keyMap input := "world123" // "world123" reversed: "321dlrow" -> "e2ld1r0w" @@ -54,7 +54,7 @@ func TestCreateSalt_Ugly(t *testing.T) { var testKeyMapMu sync.Mutex -func TestSetKeyMap_Good(t *testing.T) { +func TestLTHN_SetKeyMap_Good(t *testing.T) { testKeyMapMu.Lock() originalKeyMap := GetKeyMap() t.Cleanup(func() { diff --git a/crypt/openpgp/service.go b/crypt/openpgp/service.go index 5e1f85f..371dad3 100644 --- a/crypt/openpgp/service.go +++ b/crypt/openpgp/service.go @@ -14,17 +14,20 @@ import ( ) // Service provides OpenPGP cryptographic operations. +// Usage: use Service with the other exported helpers in this package. type Service struct { core *framework.Core } // New creates a new OpenPGP service instance. +// Usage: call New(...) to create a ready-to-use value. func New(c *framework.Core) (any, error) { return &Service{core: c}, nil } // CreateKeyPair generates a new RSA-4096 PGP keypair. // Returns the armored private key string. +// Usage: call CreateKeyPair(...) during the package's normal workflow. func (s *Service) CreateKeyPair(name, passphrase string) (string, error) { config := &packet.Config{ Algorithm: packet.PubKeyAlgoRSA, @@ -100,6 +103,7 @@ func serializeEntity(w goio.Writer, e *openpgp.Entity) error { // EncryptPGP encrypts data for a recipient identified by their public key (armored string in recipientPath). // The encrypted data is written to the provided writer and also returned as an armored string. +// Usage: call EncryptPGP(...) during the package's normal workflow. func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opts ...any) (string, error) { entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(recipientPath)) if err != nil { @@ -135,6 +139,7 @@ func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opt } // DecryptPGP decrypts a PGP message using the provided armored private key and passphrase. +// Usage: call DecryptPGP(...) during the package's normal workflow. func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any) (string, error) { entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(privateKey)) if err != nil { @@ -173,6 +178,7 @@ func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any } // HandleIPCEvents handles PGP-related IPC messages. +// Usage: call HandleIPCEvents(...) during the package's normal workflow. func (s *Service) HandleIPCEvents(c *framework.Core, msg framework.Message) error { switch m := msg.(type) { case map[string]any: diff --git a/crypt/openpgp/service_test.go b/crypt/openpgp/service_test.go index 09044ff..d023033 100644 --- a/crypt/openpgp/service_test.go +++ b/crypt/openpgp/service_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestCreateKeyPair_Good(t *testing.T) { +func TestService_CreateKeyPair_Good(t *testing.T) { c := framework.New() s := &Service{core: c} @@ -19,7 +19,7 @@ func TestCreateKeyPair_Good(t *testing.T) { assert.Contains(t, privKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----") } -func TestEncryptDecrypt_Good(t *testing.T) { +func TestService_EncryptDecrypt_Good(t *testing.T) { c := framework.New() s := &Service{core: c} diff --git a/crypt/pgp/pgp.go b/crypt/pgp/pgp.go index 56bfef8..eb09be9 100644 --- a/crypt/pgp/pgp.go +++ b/crypt/pgp/pgp.go @@ -16,6 +16,7 @@ import ( ) // KeyPair holds armored PGP public and private keys. +// Usage: use KeyPair with the other exported helpers in this package. type KeyPair struct { PublicKey string PrivateKey string @@ -24,6 +25,7 @@ type KeyPair struct { // CreateKeyPair generates a new PGP key pair for the given identity. // If password is non-empty, the private key is encrypted with it. // Returns a KeyPair with armored public and private keys. +// Usage: call CreateKeyPair(...) during the package's normal workflow. func CreateKeyPair(name, email, password string) (*KeyPair, error) { const op = "pgp.CreateKeyPair" @@ -116,6 +118,7 @@ func serializeEncryptedEntity(w io.Writer, e *openpgp.Entity) error { // Encrypt encrypts data for the recipient identified by their armored public key. // Returns the encrypted data as armored PGP output. +// Usage: call Encrypt(...) during the package's normal workflow. func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) { const op = "pgp.Encrypt" @@ -149,6 +152,7 @@ func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) { // Decrypt decrypts armored PGP data using the given armored private key. // If the private key is encrypted, the password is used to decrypt it first. +// Usage: call Decrypt(...) during the package's normal workflow. func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) { const op = "pgp.Decrypt" @@ -193,6 +197,7 @@ func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) { // Sign creates an armored detached signature for the given data using // the armored private key. If the key is encrypted, the password is used // to decrypt it first. +// Usage: call Sign(...) during the package's normal workflow. func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) { const op = "pgp.Sign" @@ -224,6 +229,7 @@ func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) { // Verify verifies an armored detached signature against the given data // and armored public key. Returns nil if the signature is valid. +// Usage: call Verify(...) during the package's normal workflow. func Verify(data, signature []byte, publicKeyArmor string) error { const op = "pgp.Verify" diff --git a/crypt/pgp/pgp_test.go b/crypt/pgp/pgp_test.go index 4f7edd9..9ab0f5e 100644 --- a/crypt/pgp/pgp_test.go +++ b/crypt/pgp/pgp_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestCreateKeyPair_Good(t *testing.T) { +func TestPGP_CreateKeyPair_Good(t *testing.T) { kp, err := CreateKeyPair("Test User", "test@example.com", "") require.NoError(t, err) require.NotNil(t, kp) @@ -15,7 +15,7 @@ func TestCreateKeyPair_Good(t *testing.T) { assert.Contains(t, kp.PrivateKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----") } -func TestCreateKeyPair_Bad(t *testing.T) { +func TestPGP_CreateKeyPair_Bad(t *testing.T) { // Empty name still works (openpgp allows it), but test with password kp, err := CreateKeyPair("Secure User", "secure@example.com", "strong-password") require.NoError(t, err) @@ -24,14 +24,14 @@ func TestCreateKeyPair_Bad(t *testing.T) { assert.Contains(t, kp.PrivateKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----") } -func TestCreateKeyPair_Ugly(t *testing.T) { +func TestPGP_CreateKeyPair_Ugly(t *testing.T) { // Minimal identity kp, err := CreateKeyPair("", "", "") require.NoError(t, err) require.NotNil(t, kp) } -func TestEncryptDecrypt_Good(t *testing.T) { +func TestPGP_EncryptDecrypt_Good(t *testing.T) { kp, err := CreateKeyPair("Test User", "test@example.com", "") require.NoError(t, err) @@ -46,7 +46,7 @@ func TestEncryptDecrypt_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestEncryptDecrypt_Bad(t *testing.T) { +func TestPGP_EncryptDecrypt_Bad(t *testing.T) { kp1, err := CreateKeyPair("User One", "one@example.com", "") require.NoError(t, err) kp2, err := CreateKeyPair("User Two", "two@example.com", "") @@ -61,7 +61,7 @@ func TestEncryptDecrypt_Bad(t *testing.T) { assert.Error(t, err) } -func TestEncryptDecrypt_Ugly(t *testing.T) { +func TestPGP_EncryptDecrypt_Ugly(t *testing.T) { // Invalid public key for encryption _, err := Encrypt([]byte("data"), "not-a-pgp-key") assert.Error(t, err) @@ -71,7 +71,7 @@ func TestEncryptDecrypt_Ugly(t *testing.T) { assert.Error(t, err) } -func TestEncryptDecryptWithPassword_Good(t *testing.T) { +func TestPGP_EncryptDecryptWithPassword_Good(t *testing.T) { password := "my-secret-passphrase" kp, err := CreateKeyPair("Secure User", "secure@example.com", password) require.NoError(t, err) @@ -85,7 +85,7 @@ func TestEncryptDecryptWithPassword_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestSignVerify_Good(t *testing.T) { +func TestPGP_SignVerify_Good(t *testing.T) { kp, err := CreateKeyPair("Signer", "signer@example.com", "") require.NoError(t, err) @@ -99,7 +99,7 @@ func TestSignVerify_Good(t *testing.T) { assert.NoError(t, err) } -func TestSignVerify_Bad(t *testing.T) { +func TestPGP_SignVerify_Bad(t *testing.T) { kp, err := CreateKeyPair("Signer", "signer@example.com", "") require.NoError(t, err) @@ -112,7 +112,7 @@ func TestSignVerify_Bad(t *testing.T) { assert.Error(t, err) } -func TestSignVerify_Ugly(t *testing.T) { +func TestPGP_SignVerify_Ugly(t *testing.T) { // Invalid key for signing _, err := Sign([]byte("data"), "not-a-key", "") assert.Error(t, err) @@ -129,7 +129,7 @@ func TestSignVerify_Ugly(t *testing.T) { assert.Error(t, err) } -func TestSignVerifyWithPassword_Good(t *testing.T) { +func TestPGP_SignVerifyWithPassword_Good(t *testing.T) { password := "signing-password" kp, err := CreateKeyPair("Signer", "signer@example.com", password) require.NoError(t, err) @@ -142,7 +142,7 @@ func TestSignVerifyWithPassword_Good(t *testing.T) { assert.NoError(t, err) } -func TestFullRoundTrip_Good(t *testing.T) { +func TestPGP_FullRoundTrip_Good(t *testing.T) { // Generate keys, encrypt, decrypt, sign, and verify - full round trip kp, err := CreateKeyPair("Full Test", "full@example.com", "") require.NoError(t, err) diff --git a/crypt/rsa/rsa.go b/crypt/rsa/rsa.go index a6534e9..2cec9cf 100644 --- a/crypt/rsa/rsa.go +++ b/crypt/rsa/rsa.go @@ -12,14 +12,17 @@ import ( ) // Service provides RSA functionality. +// Usage: use Service with the other exported helpers in this package. type Service struct{} // NewService creates and returns a new Service instance for performing RSA-related operations. +// Usage: call NewService(...) to create a ready-to-use value. func NewService() *Service { return &Service{} } // GenerateKeyPair creates a new RSA key pair. +// Usage: call GenerateKeyPair(...) during the package's normal workflow. func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err error) { const op = "rsa.GenerateKeyPair" @@ -50,6 +53,7 @@ func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err e } // Encrypt encrypts data with a public key. +// Usage: call Encrypt(...) during the package's normal workflow. func (s *Service) Encrypt(publicKey, data, label []byte) ([]byte, error) { const op = "rsa.Encrypt" @@ -77,6 +81,7 @@ func (s *Service) Encrypt(publicKey, data, label []byte) ([]byte, error) { } // Decrypt decrypts data with a private key. +// Usage: call Decrypt(...) during the package's normal workflow. func (s *Service) Decrypt(privateKey, ciphertext, label []byte) ([]byte, error) { const op = "rsa.Decrypt" diff --git a/crypt/rsa/rsa_test.go b/crypt/rsa/rsa_test.go index 8ee7460..5604fb6 100644 --- a/crypt/rsa/rsa_test.go +++ b/crypt/rsa/rsa_test.go @@ -19,7 +19,7 @@ func (r *mockReader) Read(p []byte) (n int, err error) { return 0, core.NewError("read error") } -func TestRSA_Good(t *testing.T) { +func TestRSA_RSA_Good(t *testing.T) { s := NewService() // Generate a new key pair @@ -37,7 +37,7 @@ func TestRSA_Good(t *testing.T) { assert.Equal(t, message, plaintext) } -func TestRSA_Bad(t *testing.T) { +func TestRSA_RSA_Bad(t *testing.T) { s := NewService() // Decrypt with wrong key @@ -56,7 +56,7 @@ func TestRSA_Bad(t *testing.T) { assert.Error(t, err) } -func TestRSA_Ugly(t *testing.T) { +func TestRSA_RSA_Ugly(t *testing.T) { s := NewService() // Malformed keys and messages diff --git a/crypt/symmetric.go b/crypt/symmetric.go index 65fe5f1..f0995c2 100644 --- a/crypt/symmetric.go +++ b/crypt/symmetric.go @@ -13,6 +13,7 @@ import ( // ChaCha20Encrypt encrypts plaintext using ChaCha20-Poly1305. // The key must be 32 bytes. The nonce is randomly generated and prepended // to the ciphertext. +// Usage: call ChaCha20Encrypt(...) during the package's normal workflow. func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) { aead, err := chacha20poly1305.NewX(key) if err != nil { @@ -30,6 +31,7 @@ func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) { // ChaCha20Decrypt decrypts ciphertext encrypted with ChaCha20Encrypt. // The key must be 32 bytes. Expects the nonce prepended to the ciphertext. +// Usage: call ChaCha20Decrypt(...) during the package's normal workflow. func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) { aead, err := chacha20poly1305.NewX(key) if err != nil { @@ -53,6 +55,7 @@ func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) { // AESGCMEncrypt encrypts plaintext using AES-256-GCM. // The key must be 32 bytes. The nonce is randomly generated and prepended // to the ciphertext. +// Usage: call AESGCMEncrypt(...) during the package's normal workflow. func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { @@ -75,6 +78,7 @@ func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) { // AESGCMDecrypt decrypts ciphertext encrypted with AESGCMEncrypt. // The key must be 32 bytes. Expects the nonce prepended to the ciphertext. +// Usage: call AESGCMDecrypt(...) during the package's normal workflow. func AESGCMDecrypt(ciphertext, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { diff --git a/crypt/symmetric_test.go b/crypt/symmetric_test.go index d767e48..46985f8 100644 --- a/crypt/symmetric_test.go +++ b/crypt/symmetric_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestChaCha20_Good(t *testing.T) { +func TestSymmetric_ChaCha20_Good(t *testing.T) { key := make([]byte, 32) _, err := rand.Read(key) assert.NoError(t, err) @@ -23,7 +23,7 @@ func TestChaCha20_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestChaCha20_Bad(t *testing.T) { +func TestSymmetric_ChaCha20_Bad(t *testing.T) { key := make([]byte, 32) wrongKey := make([]byte, 32) _, _ = rand.Read(key) @@ -38,7 +38,7 @@ func TestChaCha20_Bad(t *testing.T) { assert.Error(t, err) } -func TestAESGCM_Good(t *testing.T) { +func TestSymmetric_AESGCM_Good(t *testing.T) { key := make([]byte, 32) _, err := rand.Read(key) assert.NoError(t, err) @@ -56,8 +56,8 @@ func TestAESGCM_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestAESGCM_Bad_WrongKey verifies wrong key returns error, not corrupt data. -func TestAESGCM_Bad_WrongKey(t *testing.T) { +// TestSymmetric_AESGCM_Bad_WrongKey verifies wrong key returns error, not corrupt data. +func TestSymmetric_AESGCM_Bad_WrongKey(t *testing.T) { key := make([]byte, 32) wrongKey := make([]byte, 32) _, _ = rand.Read(key) @@ -72,8 +72,8 @@ func TestAESGCM_Bad_WrongKey(t *testing.T) { assert.Nil(t, decrypted, "wrong key must not return partial data") } -// TestChaCha20EmptyPlaintext_Good verifies empty plaintext round-trip at low level. -func TestChaCha20EmptyPlaintext_Good(t *testing.T) { +// TestSymmetric_ChaCha20EmptyPlaintext_Good verifies empty plaintext round-trip at low level. +func TestSymmetric_ChaCha20EmptyPlaintext_Good(t *testing.T) { key := make([]byte, 32) _, err := rand.Read(key) assert.NoError(t, err) @@ -87,8 +87,8 @@ func TestChaCha20EmptyPlaintext_Good(t *testing.T) { assert.Empty(t, decrypted) } -// TestAESGCMEmptyPlaintext_Good verifies empty plaintext round-trip at low level. -func TestAESGCMEmptyPlaintext_Good(t *testing.T) { +// TestSymmetric_AESGCMEmptyPlaintext_Good verifies empty plaintext round-trip at low level. +func TestSymmetric_AESGCMEmptyPlaintext_Good(t *testing.T) { key := make([]byte, 32) _, err := rand.Read(key) assert.NoError(t, err) @@ -102,8 +102,8 @@ func TestAESGCMEmptyPlaintext_Good(t *testing.T) { assert.Empty(t, decrypted) } -// TestChaCha20LargePayload_Good verifies 1MB encrypt/decrypt round-trip. -func TestChaCha20LargePayload_Good(t *testing.T) { +// TestSymmetric_ChaCha20LargePayload_Good verifies 1MB encrypt/decrypt round-trip. +func TestSymmetric_ChaCha20LargePayload_Good(t *testing.T) { key := make([]byte, 32) _, _ = rand.Read(key) @@ -120,8 +120,8 @@ func TestChaCha20LargePayload_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -// TestAESGCMLargePayload_Good verifies 1MB encrypt/decrypt round-trip. -func TestAESGCMLargePayload_Good(t *testing.T) { +// TestSymmetric_AESGCMLargePayload_Good verifies 1MB encrypt/decrypt round-trip. +func TestSymmetric_AESGCMLargePayload_Good(t *testing.T) { key := make([]byte, 32) _, _ = rand.Read(key) diff --git a/trust/approval.go b/trust/approval.go index e1f0495..f1dd09d 100644 --- a/trust/approval.go +++ b/trust/approval.go @@ -10,18 +10,23 @@ import ( ) // ApprovalStatus represents the state of an approval request. +// Usage: use ApprovalStatus with the other exported helpers in this package. type ApprovalStatus int const ( // ApprovalPending means the request is awaiting review. + // Usage: compare or pass ApprovalPending when using the related package APIs. ApprovalPending ApprovalStatus = iota // ApprovalApproved means the request was approved. + // Usage: compare or pass ApprovalApproved when using the related package APIs. ApprovalApproved // ApprovalDenied means the request was denied. + // Usage: compare or pass ApprovalDenied when using the related package APIs. ApprovalDenied ) // String returns the human-readable name of the approval status. +// Usage: call String(...) during the package's normal workflow. func (s ApprovalStatus) String() string { switch s { case ApprovalPending: @@ -36,6 +41,7 @@ func (s ApprovalStatus) String() string { } // ApprovalRequest represents a queued capability approval request. +// Usage: use ApprovalRequest with the other exported helpers in this package. type ApprovalRequest struct { // ID is the unique identifier for this request. ID string @@ -58,6 +64,7 @@ type ApprovalRequest struct { } // ApprovalQueue manages pending approval requests for NeedsApproval decisions. +// Usage: use ApprovalQueue with the other exported helpers in this package. type ApprovalQueue struct { mu sync.RWMutex requests map[string]*ApprovalRequest @@ -65,6 +72,7 @@ type ApprovalQueue struct { } // NewApprovalQueue creates an empty approval queue. +// Usage: call NewApprovalQueue(...) to create a ready-to-use value. func NewApprovalQueue() *ApprovalQueue { return &ApprovalQueue{ requests: make(map[string]*ApprovalRequest), @@ -73,6 +81,7 @@ func NewApprovalQueue() *ApprovalQueue { // Submit creates a new approval request and returns its ID. // Returns an error if the agent name or capability is empty. +// Usage: call Submit(...) during the package's normal workflow. func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (string, error) { if agent == "" { return "", coreerr.E("trust.ApprovalQueue.Submit", "agent name is required", nil) @@ -101,6 +110,7 @@ func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (strin // Approve marks a pending request as approved. Returns an error if the // request is not found or is not in pending status. +// Usage: call Approve(...) during the package's normal workflow. func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) error { q.mu.Lock() defer q.mu.Unlock() @@ -122,6 +132,7 @@ func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) err // Deny marks a pending request as denied. Returns an error if the // request is not found or is not in pending status. +// Usage: call Deny(...) during the package's normal workflow. func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error { q.mu.Lock() defer q.mu.Unlock() @@ -142,6 +153,7 @@ func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error } // Get returns the approval request with the given ID, or nil if not found. +// Usage: call Get(...) during the package's normal workflow. func (q *ApprovalQueue) Get(id string) *ApprovalRequest { q.mu.RLock() defer q.mu.RUnlock() @@ -156,6 +168,7 @@ func (q *ApprovalQueue) Get(id string) *ApprovalRequest { } // Pending returns all requests with ApprovalPending status. +// Usage: call Pending(...) during the package's normal workflow. func (q *ApprovalQueue) Pending() []ApprovalRequest { q.mu.RLock() defer q.mu.RUnlock() @@ -170,6 +183,7 @@ func (q *ApprovalQueue) Pending() []ApprovalRequest { } // PendingSeq returns an iterator over all requests with ApprovalPending status. +// Usage: call PendingSeq(...) during the package's normal workflow. func (q *ApprovalQueue) PendingSeq() iter.Seq[ApprovalRequest] { return func(yield func(ApprovalRequest) bool) { q.mu.RLock() @@ -186,6 +200,7 @@ func (q *ApprovalQueue) PendingSeq() iter.Seq[ApprovalRequest] { } // Len returns the total number of requests in the queue. +// Usage: call Len(...) during the package's normal workflow. func (q *ApprovalQueue) Len() int { q.mu.RLock() defer q.mu.RUnlock() diff --git a/trust/approval_test.go b/trust/approval_test.go index 6e3c344..f7bbee8 100644 --- a/trust/approval_test.go +++ b/trust/approval_test.go @@ -11,19 +11,19 @@ import ( // --- ApprovalStatus --- -func TestApprovalStatusString_Good(t *testing.T) { +func TestApproval_ApprovalStatusString_Good(t *testing.T) { assert.Equal(t, "pending", ApprovalPending.String()) assert.Equal(t, "approved", ApprovalApproved.String()) assert.Equal(t, "denied", ApprovalDenied.String()) } -func TestApprovalStatusString_Bad_Unknown(t *testing.T) { +func TestApproval_ApprovalStatusString_Bad_Unknown(t *testing.T) { assert.Contains(t, ApprovalStatus(99).String(), "unknown") } // --- Submit --- -func TestApprovalSubmit_Good(t *testing.T) { +func TestApproval_ApprovalSubmit_Good(t *testing.T) { q := NewApprovalQueue() id, err := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, err) @@ -31,7 +31,7 @@ func TestApprovalSubmit_Good(t *testing.T) { assert.Equal(t, 1, q.Len()) } -func TestApprovalSubmit_Good_MultipleRequests(t *testing.T) { +func TestApproval_ApprovalSubmit_Good_MultipleRequests(t *testing.T) { q := NewApprovalQueue() id1, err := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, err) @@ -42,7 +42,7 @@ func TestApprovalSubmit_Good_MultipleRequests(t *testing.T) { assert.Equal(t, 2, q.Len()) } -func TestApprovalSubmit_Good_EmptyRepo(t *testing.T) { +func TestApproval_ApprovalSubmit_Good_EmptyRepo(t *testing.T) { q := NewApprovalQueue() id, err := q.Submit("Clotho", CapMergePR, "") require.NoError(t, err) @@ -53,14 +53,14 @@ func TestApprovalSubmit_Good_EmptyRepo(t *testing.T) { assert.Empty(t, req.Repo) } -func TestApprovalSubmit_Bad_EmptyAgent(t *testing.T) { +func TestApproval_ApprovalSubmit_Bad_EmptyAgent(t *testing.T) { q := NewApprovalQueue() _, err := q.Submit("", CapMergePR, "") assert.Error(t, err) assert.Contains(t, err.Error(), "agent name is required") } -func TestApprovalSubmit_Bad_EmptyCapability(t *testing.T) { +func TestApproval_ApprovalSubmit_Bad_EmptyCapability(t *testing.T) { q := NewApprovalQueue() _, err := q.Submit("Clotho", "", "") assert.Error(t, err) @@ -69,7 +69,7 @@ func TestApprovalSubmit_Bad_EmptyCapability(t *testing.T) { // --- Get --- -func TestApprovalGet_Good(t *testing.T) { +func TestApproval_ApprovalGet_Good(t *testing.T) { q := NewApprovalQueue() id, err := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, err) @@ -85,7 +85,7 @@ func TestApprovalGet_Good(t *testing.T) { assert.True(t, req.ReviewedAt.IsZero()) } -func TestApprovalGet_Good_ReturnsSnapshot(t *testing.T) { +func TestApproval_ApprovalGet_Good_ReturnsSnapshot(t *testing.T) { q := NewApprovalQueue() id, err := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, err) @@ -99,14 +99,14 @@ func TestApprovalGet_Good_ReturnsSnapshot(t *testing.T) { assert.Equal(t, ApprovalPending, original.Status) } -func TestApprovalGet_Bad_NotFound(t *testing.T) { +func TestApproval_ApprovalGet_Bad_NotFound(t *testing.T) { q := NewApprovalQueue() assert.Nil(t, q.Get("nonexistent")) } // --- Approve --- -func TestApprovalApprove_Good(t *testing.T) { +func TestApproval_ApprovalApprove_Good(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") @@ -121,14 +121,14 @@ func TestApprovalApprove_Good(t *testing.T) { assert.False(t, req.ReviewedAt.IsZero()) } -func TestApprovalApprove_Bad_NotFound(t *testing.T) { +func TestApproval_ApprovalApprove_Bad_NotFound(t *testing.T) { q := NewApprovalQueue() err := q.Approve("nonexistent", "admin", "") assert.Error(t, err) assert.Contains(t, err.Error(), "not found") } -func TestApprovalApprove_Bad_AlreadyApproved(t *testing.T) { +func TestApproval_ApprovalApprove_Bad_AlreadyApproved(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, q.Approve(id, "admin", "")) @@ -138,7 +138,7 @@ func TestApprovalApprove_Bad_AlreadyApproved(t *testing.T) { assert.Contains(t, err.Error(), "already approved") } -func TestApprovalApprove_Bad_AlreadyDenied(t *testing.T) { +func TestApproval_ApprovalApprove_Bad_AlreadyDenied(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, q.Deny(id, "admin", "nope")) @@ -150,7 +150,7 @@ func TestApprovalApprove_Bad_AlreadyDenied(t *testing.T) { // --- Deny --- -func TestApprovalDeny_Good(t *testing.T) { +func TestApproval_ApprovalDeny_Good(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") @@ -165,14 +165,14 @@ func TestApprovalDeny_Good(t *testing.T) { assert.False(t, req.ReviewedAt.IsZero()) } -func TestApprovalDeny_Bad_NotFound(t *testing.T) { +func TestApproval_ApprovalDeny_Bad_NotFound(t *testing.T) { q := NewApprovalQueue() err := q.Deny("nonexistent", "admin", "") assert.Error(t, err) assert.Contains(t, err.Error(), "not found") } -func TestApprovalDeny_Bad_AlreadyDenied(t *testing.T) { +func TestApproval_ApprovalDeny_Bad_AlreadyDenied(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, q.Deny(id, "admin", "")) @@ -184,7 +184,7 @@ func TestApprovalDeny_Bad_AlreadyDenied(t *testing.T) { // --- Pending --- -func TestApprovalPending_Good(t *testing.T) { +func TestApproval_ApprovalPending_Good(t *testing.T) { q := NewApprovalQueue() q.Submit("Clotho", CapMergePR, "host-uk/core") q.Submit("Hypnos", CapMergePR, "host-uk/docs") @@ -196,12 +196,12 @@ func TestApprovalPending_Good(t *testing.T) { assert.Len(t, pending, 2) } -func TestApprovalPending_Good_Empty(t *testing.T) { +func TestApproval_ApprovalPending_Good_Empty(t *testing.T) { q := NewApprovalQueue() assert.Empty(t, q.Pending()) } -func TestApprovalPendingSeq_Good(t *testing.T) { +func TestApproval_ApprovalPendingSeq_Good(t *testing.T) { q := NewApprovalQueue() q.Submit("Clotho", CapMergePR, "host-uk/core") q.Submit("Hypnos", CapMergePR, "host-uk/docs") @@ -219,7 +219,7 @@ func TestApprovalPendingSeq_Good(t *testing.T) { // --- Concurrent operations --- -func TestApprovalConcurrent_Good(t *testing.T) { +func TestApproval_ApprovalConcurrent_Good(t *testing.T) { q := NewApprovalQueue() const n = 10 @@ -270,7 +270,7 @@ func TestApprovalConcurrent_Good(t *testing.T) { // --- Integration: PolicyEngine + ApprovalQueue --- -func TestApprovalWorkflow_Good_EndToEnd(t *testing.T) { +func TestApproval_ApprovalWorkflow_Good_EndToEnd(t *testing.T) { pe := newTestEngine(t) q := NewApprovalQueue() @@ -293,7 +293,7 @@ func TestApprovalWorkflow_Good_EndToEnd(t *testing.T) { assert.Equal(t, "Virgil", req.ReviewedBy) } -func TestApprovalWorkflow_Good_DenyEndToEnd(t *testing.T) { +func TestApproval_ApprovalWorkflow_Good_DenyEndToEnd(t *testing.T) { pe := newTestEngine(t) q := NewApprovalQueue() diff --git a/trust/audit.go b/trust/audit.go index 06b9032..3e74f4d 100644 --- a/trust/audit.go +++ b/trust/audit.go @@ -11,6 +11,7 @@ import ( ) // AuditEntry records a single policy evaluation for compliance. +// Usage: use AuditEntry with the other exported helpers in this package. type AuditEntry struct { // Timestamp is when the evaluation occurred. Timestamp time.Time `json:"timestamp"` @@ -27,6 +28,7 @@ type AuditEntry struct { } // MarshalJSON implements custom JSON encoding for Decision. +// Usage: call MarshalJSON(...) during the package's normal workflow. func (d Decision) MarshalJSON() ([]byte, error) { result := core.JSONMarshal(d.String()) if !result.OK { @@ -37,6 +39,7 @@ func (d Decision) MarshalJSON() ([]byte, error) { } // UnmarshalJSON implements custom JSON decoding for Decision. +// Usage: call UnmarshalJSON(...) during the package's normal workflow. func (d *Decision) UnmarshalJSON(data []byte) error { var s string result := core.JSONUnmarshal(data, &s) @@ -58,6 +61,7 @@ func (d *Decision) UnmarshalJSON(data []byte) error { } // AuditLog is an append-only log of policy evaluations. +// Usage: use AuditLog with the other exported helpers in this package. type AuditLog struct { mu sync.Mutex entries []AuditEntry @@ -66,6 +70,7 @@ type AuditLog struct { // NewAuditLog creates an in-memory audit log. If a writer is provided, // each entry is also written as a JSON line to that writer (append-only). +// Usage: call NewAuditLog(...) to create a ready-to-use value. func NewAuditLog(w io.Writer) *AuditLog { return &AuditLog{ writer: w, @@ -73,6 +78,7 @@ func NewAuditLog(w io.Writer) *AuditLog { } // Record appends an evaluation result to the audit log. +// Usage: call Record(...) during the package's normal workflow. func (l *AuditLog) Record(result EvalResult, repo string) error { entry := AuditEntry{ Timestamp: time.Now(), @@ -104,6 +110,7 @@ func (l *AuditLog) Record(result EvalResult, repo string) error { } // Entries returns a snapshot of all audit entries. +// Usage: call Entries(...) during the package's normal workflow. func (l *AuditLog) Entries() []AuditEntry { l.mu.Lock() defer l.mu.Unlock() @@ -114,6 +121,7 @@ func (l *AuditLog) Entries() []AuditEntry { } // EntriesSeq returns an iterator over all audit entries. +// Usage: call EntriesSeq(...) during the package's normal workflow. func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] { return func(yield func(AuditEntry) bool) { l.mu.Lock() @@ -128,6 +136,7 @@ func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] { } // Len returns the number of entries in the log. +// Usage: call Len(...) during the package's normal workflow. func (l *AuditLog) Len() int { l.mu.Lock() defer l.mu.Unlock() @@ -135,6 +144,7 @@ func (l *AuditLog) Len() int { } // EntriesFor returns all audit entries for a specific agent. +// Usage: call EntriesFor(...) during the package's normal workflow. func (l *AuditLog) EntriesFor(agent string) []AuditEntry { l.mu.Lock() defer l.mu.Unlock() @@ -149,6 +159,7 @@ func (l *AuditLog) EntriesFor(agent string) []AuditEntry { } // EntriesForSeq returns an iterator over audit entries for a specific agent. +// Usage: call EntriesForSeq(...) during the package's normal workflow. func (l *AuditLog) EntriesForSeq(agent string) iter.Seq[AuditEntry] { return func(yield func(AuditEntry) bool) { l.mu.Lock() diff --git a/trust/audit_test.go b/trust/audit_test.go index 23c9342..583a461 100644 --- a/trust/audit_test.go +++ b/trust/audit_test.go @@ -12,7 +12,7 @@ import ( // --- AuditLog basic --- -func TestAuditRecord_Good(t *testing.T) { +func TestAudit_AuditRecord_Good(t *testing.T) { log := NewAuditLog(nil) result := EvalResult{ @@ -26,7 +26,7 @@ func TestAuditRecord_Good(t *testing.T) { assert.Equal(t, 1, log.Len()) } -func TestAuditRecord_Good_EntryFields(t *testing.T) { +func TestAudit_AuditRecord_Good_EntryFields(t *testing.T) { log := NewAuditLog(nil) result := EvalResult{ @@ -50,7 +50,7 @@ func TestAuditRecord_Good_EntryFields(t *testing.T) { assert.False(t, e.Timestamp.IsZero()) } -func TestAuditRecord_Good_NoRepo(t *testing.T) { +func TestAudit_AuditRecord_Good_NoRepo(t *testing.T) { log := NewAuditLog(nil) result := EvalResult{ Decision: Allow, @@ -66,7 +66,7 @@ func TestAuditRecord_Good_NoRepo(t *testing.T) { assert.Empty(t, entries[0].Repo) } -func TestAuditEntries_Good_Snapshot(t *testing.T) { +func TestAudit_AuditEntries_Good_Snapshot(t *testing.T) { log := NewAuditLog(nil) log.Record(EvalResult{Agent: "A", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "") @@ -78,12 +78,12 @@ func TestAuditEntries_Good_Snapshot(t *testing.T) { assert.Equal(t, "A", log.Entries()[0].Agent) } -func TestAuditEntries_Good_Empty(t *testing.T) { +func TestAudit_AuditEntries_Good_Empty(t *testing.T) { log := NewAuditLog(nil) assert.Empty(t, log.Entries()) } -func TestAuditEntries_Good_AppendOnly(t *testing.T) { +func TestAudit_AuditEntries_Good_AppendOnly(t *testing.T) { log := NewAuditLog(nil) for i := range 5 { @@ -99,7 +99,7 @@ func TestAuditEntries_Good_AppendOnly(t *testing.T) { // --- EntriesFor --- -func TestAuditEntriesFor_Good(t *testing.T) { +func TestAudit_AuditEntriesFor_Good(t *testing.T) { log := NewAuditLog(nil) log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "") @@ -121,7 +121,7 @@ func TestAuditEntriesFor_Good(t *testing.T) { assert.Equal(t, 2, count) } -func TestAuditEntriesSeq_Good(t *testing.T) { +func TestAudit_AuditEntriesSeq_Good(t *testing.T) { log := NewAuditLog(nil) log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "") log.Record(EvalResult{Agent: "Clotho", Cap: CapCreatePR, Decision: Allow, Reason: "ok"}, "") @@ -133,7 +133,7 @@ func TestAuditEntriesSeq_Good(t *testing.T) { assert.Equal(t, 2, count) } -func TestAuditEntriesFor_Bad_NotFound(t *testing.T) { +func TestAudit_AuditEntriesFor_Bad_NotFound(t *testing.T) { log := NewAuditLog(nil) log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "") @@ -142,7 +142,7 @@ func TestAuditEntriesFor_Bad_NotFound(t *testing.T) { // --- Writer output --- -func TestAuditRecord_Good_WritesToWriter(t *testing.T) { +func TestAudit_AuditRecord_Good_WritesToWriter(t *testing.T) { buf := core.NewBuilder() log := NewAuditLog(buf) @@ -168,7 +168,7 @@ func TestAuditRecord_Good_WritesToWriter(t *testing.T) { assert.Equal(t, "host-uk/core", entry.Repo) } -func TestAuditRecord_Good_MultipleLines(t *testing.T) { +func TestAudit_AuditRecord_Good_MultipleLines(t *testing.T) { buf := core.NewBuilder() log := NewAuditLog(buf) @@ -192,7 +192,7 @@ func TestAuditRecord_Good_MultipleLines(t *testing.T) { } } -func TestAuditRecord_Bad_WriterError(t *testing.T) { +func TestAudit_AuditRecord_Bad_WriterError(t *testing.T) { log := NewAuditLog(&failWriter{}) result := EvalResult{ @@ -218,7 +218,7 @@ func (f *failWriter) Write(_ []byte) (int, error) { // --- Decision JSON marshalling --- -func TestDecisionJSON_Good_RoundTrip(t *testing.T) { +func TestAudit_DecisionJSON_Good_RoundTrip(t *testing.T) { decisions := []Decision{Deny, Allow, NeedsApproval} expected := []string{`"deny"`, `"allow"`, `"needs_approval"`} @@ -234,7 +234,7 @@ func TestDecisionJSON_Good_RoundTrip(t *testing.T) { } } -func TestDecisionJSON_Bad_UnknownString(t *testing.T) { +func TestAudit_DecisionJSON_Bad_UnknownString(t *testing.T) { var d Decision result := core.JSONUnmarshal([]byte(`"invalid"`), &d) err, _ := result.Value.(error) @@ -242,7 +242,7 @@ func TestDecisionJSON_Bad_UnknownString(t *testing.T) { assert.Contains(t, err.Error(), "unknown decision") } -func TestDecisionJSON_Bad_NonString(t *testing.T) { +func TestAudit_DecisionJSON_Bad_NonString(t *testing.T) { var d Decision result := core.JSONUnmarshal([]byte(`42`), &d) err, _ := result.Value.(error) @@ -251,7 +251,7 @@ func TestDecisionJSON_Bad_NonString(t *testing.T) { // --- Concurrent audit logging --- -func TestAuditConcurrent_Good(t *testing.T) { +func TestAudit_AuditConcurrent_Good(t *testing.T) { buf := core.NewBuilder() log := NewAuditLog(buf) @@ -277,7 +277,7 @@ func TestAuditConcurrent_Good(t *testing.T) { // --- Integration: PolicyEngine + AuditLog --- -func TestAuditPolicyIntegration_Good(t *testing.T) { +func TestAudit_AuditPolicyIntegration_Good(t *testing.T) { buf := core.NewBuilder() log := NewAuditLog(buf) pe := newTestEngine(t) diff --git a/trust/config.go b/trust/config.go index ea2618c..b252fc9 100644 --- a/trust/config.go +++ b/trust/config.go @@ -8,6 +8,7 @@ import ( ) // PolicyConfig is the JSON-serialisable representation of a trust policy. +// Usage: use PolicyConfig with the other exported helpers in this package. type PolicyConfig struct { Tier int `json:"tier"` Allowed []string `json:"allowed"` @@ -16,11 +17,13 @@ type PolicyConfig struct { } // PoliciesConfig is the top-level configuration containing all tier policies. +// Usage: use PoliciesConfig with the other exported helpers in this package. type PoliciesConfig struct { Policies []PolicyConfig `json:"policies"` } // LoadPoliciesFromFile reads a JSON file and returns parsed policies. +// Usage: call LoadPoliciesFromFile(...) during the package's normal workflow. func LoadPoliciesFromFile(path string) ([]Policy, error) { openResult := (&core.Fs{}).New("/").Open(path) if !openResult.OK { @@ -31,6 +34,7 @@ func LoadPoliciesFromFile(path string) ([]Policy, error) { } // LoadPolicies reads JSON from a reader and returns parsed policies. +// Usage: call LoadPolicies(...) during the package's normal workflow. func LoadPolicies(r io.Reader) ([]Policy, error) { readResult := core.ReadAll(r) if !readResult.OK { @@ -76,6 +80,7 @@ func convertPolicies(cfg PoliciesConfig) ([]Policy, error) { // ApplyPolicies loads policies from a reader and sets them on the engine, // replacing any existing policies for the same tiers. +// Usage: call ApplyPolicies(...) during the package's normal workflow. func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error { policies, err := LoadPolicies(r) if err != nil { @@ -90,6 +95,7 @@ func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error { } // ApplyPoliciesFromFile loads policies from a JSON file and sets them on the engine. +// Usage: call ApplyPoliciesFromFile(...) during the package's normal workflow. func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error { openResult := (&core.Fs{}).New("/").Open(path) if !openResult.OK { @@ -100,6 +106,7 @@ func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error { } // ExportPolicies serialises the current policies as JSON to the given writer. +// Usage: call ExportPolicies(...) during the package's normal workflow. func (pe *PolicyEngine) ExportPolicies(w io.Writer) error { var cfg PoliciesConfig for _, tier := range []Tier{TierUntrusted, TierVerified, TierFull} { diff --git a/trust/config_test.go b/trust/config_test.go index 72825ba..b4a0e01 100644 --- a/trust/config_test.go +++ b/trust/config_test.go @@ -30,13 +30,13 @@ const validPolicyJSON = `{ // --- LoadPolicies --- -func TestLoadPolicies_Good(t *testing.T) { +func TestConfig_LoadPolicies_Good(t *testing.T) { policies, err := LoadPolicies(core.NewReader(validPolicyJSON)) require.NoError(t, err) assert.Len(t, policies, 3) } -func TestLoadPolicies_Good_FieldMapping(t *testing.T) { +func TestConfig_LoadPolicies_Good_FieldMapping(t *testing.T) { policies, err := LoadPolicies(core.NewReader(validPolicyJSON)) require.NoError(t, err) @@ -60,33 +60,33 @@ func TestLoadPolicies_Good_FieldMapping(t *testing.T) { assert.Len(t, policies[2].Denied, 2) } -func TestLoadPolicies_Good_EmptyPolicies(t *testing.T) { +func TestConfig_LoadPolicies_Good_EmptyPolicies(t *testing.T) { input := `{"policies": []}` policies, err := LoadPolicies(core.NewReader(input)) require.NoError(t, err) assert.Empty(t, policies) } -func TestLoadPolicies_Bad_InvalidJSON(t *testing.T) { +func TestConfig_LoadPolicies_Bad_InvalidJSON(t *testing.T) { _, err := LoadPolicies(core.NewReader(`{invalid`)) assert.Error(t, err) } -func TestLoadPolicies_Bad_InvalidTier(t *testing.T) { +func TestConfig_LoadPolicies_Bad_InvalidTier(t *testing.T) { input := `{"policies": [{"tier": 0, "allowed": ["repo.push"]}]}` _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -func TestLoadPolicies_Bad_TierTooHigh(t *testing.T) { +func TestConfig_LoadPolicies_Bad_TierTooHigh(t *testing.T) { input := `{"policies": [{"tier": 99, "allowed": ["repo.push"]}]}` _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -func TestLoadPolicies_Bad_UnknownField(t *testing.T) { +func TestConfig_LoadPolicies_Bad_UnknownField(t *testing.T) { input := `{"policies": [{"tier": 1, "allowed": ["repo.push"], "bogus": true}]}` _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err, "DisallowUnknownFields should reject unknown fields") @@ -94,7 +94,7 @@ func TestLoadPolicies_Bad_UnknownField(t *testing.T) { // --- LoadPoliciesFromFile --- -func TestLoadPoliciesFromFile_Good(t *testing.T) { +func TestConfig_LoadPoliciesFromFile_Good(t *testing.T) { dir := t.TempDir() path := core.Path(dir, "policies.json") writePolicyFile(t, path, validPolicyJSON) @@ -104,14 +104,14 @@ func TestLoadPoliciesFromFile_Good(t *testing.T) { assert.Len(t, policies, 3) } -func TestLoadPoliciesFromFile_Bad_NotFound(t *testing.T) { +func TestConfig_LoadPoliciesFromFile_Bad_NotFound(t *testing.T) { _, err := LoadPoliciesFromFile("/nonexistent/path/policies.json") assert.Error(t, err) } // --- ApplyPolicies --- -func TestApplyPolicies_Good(t *testing.T) { +func TestConfig_ApplyPolicies_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "TestAgent", Tier: TierVerified})) pe := NewPolicyEngine(r) @@ -135,7 +135,7 @@ func TestApplyPolicies_Good(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestApplyPolicies_Bad_InvalidJSON(t *testing.T) { +func TestConfig_ApplyPolicies_Bad_InvalidJSON(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) @@ -143,7 +143,7 @@ func TestApplyPolicies_Bad_InvalidJSON(t *testing.T) { assert.Error(t, err) } -func TestApplyPolicies_Bad_InvalidTier(t *testing.T) { +func TestConfig_ApplyPolicies_Bad_InvalidTier(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) @@ -154,7 +154,7 @@ func TestApplyPolicies_Bad_InvalidTier(t *testing.T) { // --- ApplyPoliciesFromFile --- -func TestApplyPoliciesFromFile_Good(t *testing.T) { +func TestConfig_ApplyPoliciesFromFile_Good(t *testing.T) { dir := t.TempDir() path := core.Path(dir, "policies.json") writePolicyFile(t, path, validPolicyJSON) @@ -172,7 +172,7 @@ func TestApplyPoliciesFromFile_Good(t *testing.T) { assert.Len(t, p.Allowed, 3) } -func TestApplyPoliciesFromFile_Bad_NotFound(t *testing.T) { +func TestConfig_ApplyPoliciesFromFile_Bad_NotFound(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) err := pe.ApplyPoliciesFromFile("/nonexistent/policies.json") @@ -181,7 +181,7 @@ func TestApplyPoliciesFromFile_Bad_NotFound(t *testing.T) { // --- ExportPolicies --- -func TestExportPolicies_Good(t *testing.T) { +func TestConfig_ExportPolicies_Good(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) // loads defaults @@ -196,7 +196,7 @@ func TestExportPolicies_Good(t *testing.T) { assert.Len(t, cfg.Policies, 3) } -func TestExportPolicies_Good_RoundTrip(t *testing.T) { +func TestConfig_ExportPolicies_Good_RoundTrip(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "A", Tier: TierFull})) pe := NewPolicyEngine(r) @@ -232,26 +232,26 @@ func writePolicyFile(t *testing.T, path, content string) { // --- Helper conversion --- -func TestToCapabilities_Good(t *testing.T) { +func TestConfig_ToCapabilities_Good(t *testing.T) { caps := toCapabilities([]string{"repo.push", "pr.merge"}) assert.Len(t, caps, 2) assert.Equal(t, CapPushRepo, caps[0]) assert.Equal(t, CapMergePR, caps[1]) } -func TestToCapabilities_Good_Empty(t *testing.T) { +func TestConfig_ToCapabilities_Good_Empty(t *testing.T) { assert.Nil(t, toCapabilities(nil)) assert.Nil(t, toCapabilities([]string{})) } -func TestFromCapabilities_Good(t *testing.T) { +func TestConfig_FromCapabilities_Good(t *testing.T) { ss := fromCapabilities([]Capability{CapPushRepo, CapMergePR}) assert.Len(t, ss, 2) assert.Equal(t, "repo.push", ss[0]) assert.Equal(t, "pr.merge", ss[1]) } -func TestFromCapabilities_Good_Empty(t *testing.T) { +func TestConfig_FromCapabilities_Good_Empty(t *testing.T) { assert.Nil(t, fromCapabilities(nil)) assert.Nil(t, fromCapabilities([]Capability{})) } diff --git a/trust/policy.go b/trust/policy.go index bc72760..c135276 100644 --- a/trust/policy.go +++ b/trust/policy.go @@ -8,6 +8,7 @@ import ( ) // Policy defines the access rules for a given trust tier. +// Usage: use Policy with the other exported helpers in this package. type Policy struct { // Tier is the trust level this policy applies to. Tier Tier @@ -20,24 +21,30 @@ type Policy struct { } // PolicyEngine evaluates capability requests against registered policies. +// Usage: use PolicyEngine with the other exported helpers in this package. type PolicyEngine struct { registry *Registry policies map[Tier]*Policy } // Decision is the result of a policy evaluation. +// Usage: use Decision with the other exported helpers in this package. type Decision int const ( // Deny means the action is not permitted. + // Usage: compare or pass Deny when using the related package APIs. Deny Decision = iota // Allow means the action is permitted. + // Usage: compare or pass Allow when using the related package APIs. Allow // NeedsApproval means the action requires human or higher-tier approval. + // Usage: compare or pass NeedsApproval when using the related package APIs. NeedsApproval ) // String returns the human-readable name of the decision. +// Usage: call String(...) during the package's normal workflow. func (d Decision) String() string { switch d { case Deny: @@ -52,6 +59,7 @@ func (d Decision) String() string { } // EvalResult contains the outcome of a capability evaluation. +// Usage: use EvalResult with the other exported helpers in this package. type EvalResult struct { Decision Decision Agent string @@ -60,6 +68,7 @@ type EvalResult struct { } // NewPolicyEngine creates a policy engine with the given registry and default policies. +// Usage: call NewPolicyEngine(...) to create a ready-to-use value. func NewPolicyEngine(registry *Registry) *PolicyEngine { pe := &PolicyEngine{ registry: registry, @@ -72,6 +81,7 @@ func NewPolicyEngine(registry *Registry) *PolicyEngine { // Evaluate checks whether the named agent can perform the given capability. // If the agent has scoped repos and the capability is repo-scoped, the repo // parameter is checked against the agent's allowed repos. +// Usage: call Evaluate(...) during the package's normal workflow. func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) EvalResult { agent := pe.registry.Get(agentName) if agent == nil { @@ -145,6 +155,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) } // SetPolicy replaces the policy for a given tier. +// Usage: call SetPolicy(...) during the package's normal workflow. func (pe *PolicyEngine) SetPolicy(p Policy) error { if !p.Tier.Valid() { return coreerr.E("trust.SetPolicy", core.Sprintf("invalid tier %d", p.Tier), nil) @@ -154,6 +165,7 @@ func (pe *PolicyEngine) SetPolicy(p Policy) error { } // GetPolicy returns the policy for a tier, or nil if none is set. +// Usage: call GetPolicy(...) during the package's normal workflow. func (pe *PolicyEngine) GetPolicy(t Tier) *Policy { return pe.policies[t] } diff --git a/trust/policy_test.go b/trust/policy_test.go index c656e89..19ff5f7 100644 --- a/trust/policy_test.go +++ b/trust/policy_test.go @@ -29,19 +29,19 @@ func newTestEngine(t *testing.T) *PolicyEngine { // --- Decision --- -func TestDecisionString_Good(t *testing.T) { +func TestPolicy_DecisionString_Good(t *testing.T) { assert.Equal(t, "deny", Deny.String()) assert.Equal(t, "allow", Allow.String()) assert.Equal(t, "needs_approval", NeedsApproval.String()) } -func TestDecisionString_Bad_Unknown(t *testing.T) { +func TestPolicy_DecisionString_Bad_Unknown(t *testing.T) { assert.Contains(t, Decision(99).String(), "unknown") } // --- Tier 3 (Full Trust) --- -func TestEvaluate_Good_Tier3CanDoAnything(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier3CanDoAnything(t *testing.T) { pe := newTestEngine(t) caps := []Capability{ @@ -57,56 +57,56 @@ func TestEvaluate_Good_Tier3CanDoAnything(t *testing.T) { // --- Tier 2 (Verified) --- -func TestEvaluate_Good_Tier2CanCreatePR(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier2CanCreatePR(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapCreatePR, "host-uk/core") assert.Equal(t, Allow, result.Decision) } -func TestEvaluate_Good_Tier2CanPushToScopedRepo(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier2CanPushToScopedRepo(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapPushRepo, "host-uk/core") assert.Equal(t, Allow, result.Decision) } -func TestEvaluate_Good_Tier2NeedsApprovalToMerge(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier2NeedsApprovalToMerge(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapMergePR, "host-uk/core") assert.Equal(t, NeedsApproval, result.Decision) } -func TestEvaluate_Good_Tier2CanCreateIssue(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier2CanCreateIssue(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapCreateIssue, "") assert.Equal(t, Allow, result.Decision) } -func TestEvaluate_Bad_Tier2CannotAccessWorkspace(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2CannotAccessWorkspace(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapAccessWorkspace, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier2CannotModifyFlows(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2CannotModifyFlows(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapModifyFlows, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier2CannotRunPrivileged(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2CannotRunPrivileged(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapRunPrivileged, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier2CannotPushToUnscopedRepo(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2CannotPushToUnscopedRepo(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapPushRepo, "host-uk/secret-repo") assert.Equal(t, Deny, result.Decision) assert.Contains(t, result.Reason, "does not have access") } -func TestEvaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) { pe := newTestEngine(t) // Push without specifying a repo should be denied for scoped agents. result := pe.Evaluate("Clotho", CapPushRepo, "") @@ -115,43 +115,43 @@ func TestEvaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) { // --- Tier 1 (Untrusted) --- -func TestEvaluate_Good_Tier1CanCreatePR(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier1CanCreatePR(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapCreatePR, "") assert.Equal(t, Allow, result.Decision) } -func TestEvaluate_Good_Tier1CanCommentIssue(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier1CanCommentIssue(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapCommentIssue, "") assert.Equal(t, Allow, result.Decision) } -func TestEvaluate_Bad_Tier1CannotPush(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier1CannotPush(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapPushRepo, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier1CannotMerge(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier1CannotMerge(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapMergePR, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier1CannotCreateIssue(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier1CannotCreateIssue(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapCreateIssue, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier1CannotReadSecrets(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier1CannotReadSecrets(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapReadSecrets, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapRunPrivileged, "") assert.Equal(t, Deny, result.Decision) @@ -159,14 +159,14 @@ func TestEvaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) { // --- Edge cases --- -func TestEvaluate_Bad_UnknownAgent(t *testing.T) { +func TestPolicy_Evaluate_Bad_UnknownAgent(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Unknown", CapCreatePR, "") assert.Equal(t, Deny, result.Decision) assert.Contains(t, result.Reason, "not registered") } -func TestEvaluate_Good_EvalResultFields(t *testing.T) { +func TestPolicy_Evaluate_Good_EvalResultFields(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Athena", CapPushRepo, "") assert.Equal(t, "Athena", result.Agent) @@ -176,7 +176,7 @@ func TestEvaluate_Good_EvalResultFields(t *testing.T) { // --- SetPolicy --- -func TestSetPolicy_Good(t *testing.T) { +func TestPolicy_SetPolicy_Good(t *testing.T) { pe := newTestEngine(t) err := pe.SetPolicy(Policy{ Tier: TierVerified, @@ -189,64 +189,64 @@ func TestSetPolicy_Good(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestSetPolicy_Bad_InvalidTier(t *testing.T) { +func TestPolicy_SetPolicy_Bad_InvalidTier(t *testing.T) { pe := newTestEngine(t) err := pe.SetPolicy(Policy{Tier: Tier(0)}) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -func TestGetPolicy_Good(t *testing.T) { +func TestPolicy_GetPolicy_Good(t *testing.T) { pe := newTestEngine(t) p := pe.GetPolicy(TierFull) require.NotNil(t, p) assert.Equal(t, TierFull, p.Tier) } -func TestGetPolicy_Bad_NotFound(t *testing.T) { +func TestPolicy_GetPolicy_Bad_NotFound(t *testing.T) { pe := newTestEngine(t) assert.Nil(t, pe.GetPolicy(Tier(99))) } // --- isRepoScoped / repoAllowed helpers --- -func TestIsRepoScoped_Good(t *testing.T) { +func TestPolicy_IsRepoScoped_Good(t *testing.T) { assert.True(t, isRepoScoped(CapPushRepo)) assert.True(t, isRepoScoped(CapCreatePR)) assert.True(t, isRepoScoped(CapMergePR)) assert.True(t, isRepoScoped(CapReadSecrets)) } -func TestIsRepoScoped_Bad_NotScoped(t *testing.T) { +func TestPolicy_IsRepoScoped_Bad_NotScoped(t *testing.T) { assert.False(t, isRepoScoped(CapRunPrivileged)) assert.False(t, isRepoScoped(CapAccessWorkspace)) assert.False(t, isRepoScoped(CapModifyFlows)) } -func TestRepoAllowed_Good(t *testing.T) { +func TestPolicy_RepoAllowed_Good(t *testing.T) { scoped := []string{"host-uk/core", "host-uk/docs"} assert.True(t, repoAllowed(scoped, "host-uk/core")) assert.True(t, repoAllowed(scoped, "host-uk/docs")) } -func TestRepoAllowed_Bad_NotInScope(t *testing.T) { +func TestPolicy_RepoAllowed_Bad_NotInScope(t *testing.T) { scoped := []string{"host-uk/core"} assert.False(t, repoAllowed(scoped, "host-uk/secret")) } -func TestRepoAllowed_Bad_EmptyRepo(t *testing.T) { +func TestPolicy_RepoAllowed_Bad_EmptyRepo(t *testing.T) { scoped := []string{"host-uk/core"} assert.False(t, repoAllowed(scoped, "")) } -func TestRepoAllowed_Bad_EmptyScope(t *testing.T) { +func TestPolicy_RepoAllowed_Bad_EmptyScope(t *testing.T) { assert.False(t, repoAllowed(nil, "host-uk/core")) assert.False(t, repoAllowed([]string{}, "host-uk/core")) } // --- Tier 3 ignores repo scoping --- -func TestEvaluate_Good_Tier3IgnoresRepoScope(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier3IgnoresRepoScope(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "Virgil", @@ -261,7 +261,7 @@ func TestEvaluate_Good_Tier3IgnoresRepoScope(t *testing.T) { // --- Default rate limits --- -func TestDefaultRateLimit_Good(t *testing.T) { +func TestPolicy_DefaultRateLimit_Good(t *testing.T) { assert.Equal(t, 10, defaultRateLimit(TierUntrusted)) assert.Equal(t, 60, defaultRateLimit(TierVerified)) assert.Equal(t, 0, defaultRateLimit(TierFull)) @@ -270,11 +270,11 @@ func TestDefaultRateLimit_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll verifies that a Tier 2 +// TestPolicy_Evaluate_Good_Tier2EmptyScopedReposAllowsAll verifies that a Tier 2 // agent with empty ScopedRepos is treated as "unrestricted" for repo-scoped // capabilities. NOTE: This is a potential security concern documented in // FINDINGS.md — empty ScopedRepos bypasses the repo scope check entirely. -func TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "Hypnos", @@ -301,9 +301,9 @@ func TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -// TestEvaluate_Bad_CapabilityNotInAnyList verifies that a capability not in +// TestPolicy_Evaluate_Bad_CapabilityNotInAnyList verifies that a capability not in // allowed, denied, or requires_approval lists defaults to deny. -func TestEvaluate_Bad_CapabilityNotInAnyList(t *testing.T) { +func TestPolicy_Evaluate_Bad_CapabilityNotInAnyList(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "TestAgent", @@ -325,9 +325,9 @@ func TestEvaluate_Bad_CapabilityNotInAnyList(t *testing.T) { assert.Contains(t, result.Reason, "not granted") } -// TestEvaluate_Bad_UnknownCapability verifies that a completely invented +// TestPolicy_Evaluate_Bad_UnknownCapability verifies that a completely invented // capability string is denied. -func TestEvaluate_Bad_UnknownCapability(t *testing.T) { +func TestPolicy_Evaluate_Bad_UnknownCapability(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Athena", Capability("nonexistent.capability"), "") @@ -335,9 +335,9 @@ func TestEvaluate_Bad_UnknownCapability(t *testing.T) { assert.Contains(t, result.Reason, "not granted") } -// TestConcurrentEvaluate_Good verifies that concurrent policy evaluations +// TestPolicy_ConcurrentEvaluate_Good verifies that concurrent policy evaluations // with 10 goroutines do not race. -func TestConcurrentEvaluate_Good(t *testing.T) { +func TestPolicy_ConcurrentEvaluate_Good(t *testing.T) { pe := newTestEngine(t) const n = 10 @@ -360,10 +360,10 @@ func TestConcurrentEvaluate_Good(t *testing.T) { wg.Wait() } -// TestEvaluate_Bad_Tier2ScopedReposWithEmptyRepoParam verifies that +// TestPolicy_Evaluate_Bad_Tier2ScopedReposWithEmptyRepoParam verifies that // a scoped agent requesting a repo-scoped capability without specifying // the repo is denied. -func TestEvaluate_Bad_Tier2ScopedReposWithEmptyRepoParam(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2ScopedReposWithEmptyRepoParam(t *testing.T) { pe := newTestEngine(t) // Clotho has ScopedRepos but passes empty repo diff --git a/trust/scope_test.go b/trust/scope_test.go index db9f758..d2de46d 100644 --- a/trust/scope_test.go +++ b/trust/scope_test.go @@ -9,100 +9,100 @@ import ( // --- matchScope --- -func TestMatchScope_Good_ExactMatch(t *testing.T) { +func TestScope_MatchScope_Good_ExactMatch(t *testing.T) { assert.True(t, matchScope("host-uk/core", "host-uk/core")) } -func TestMatchScope_Good_SingleWildcard(t *testing.T) { +func TestScope_MatchScope_Good_SingleWildcard(t *testing.T) { assert.True(t, matchScope("core/*", "core/php")) assert.True(t, matchScope("core/*", "core/go-crypt")) assert.True(t, matchScope("host-uk/*", "host-uk/core")) } -func TestMatchScope_Good_RecursiveWildcard(t *testing.T) { +func TestScope_MatchScope_Good_RecursiveWildcard(t *testing.T) { assert.True(t, matchScope("core/**", "core/php")) assert.True(t, matchScope("core/**", "core/php/sub")) assert.True(t, matchScope("core/**", "core/a/b/c")) } -func TestMatchScope_Bad_ExactMismatch(t *testing.T) { +func TestScope_MatchScope_Bad_ExactMismatch(t *testing.T) { assert.False(t, matchScope("host-uk/core", "host-uk/docs")) } -func TestMatchScope_Bad_SingleWildcardNoNested(t *testing.T) { +func TestScope_MatchScope_Bad_SingleWildcardNoNested(t *testing.T) { // "core/*" should NOT match "core/php/sub" — only single level. assert.False(t, matchScope("core/*", "core/php/sub")) assert.False(t, matchScope("core/*", "core/a/b")) } -func TestMatchScope_Bad_SingleWildcardNoPrefix(t *testing.T) { +func TestScope_MatchScope_Bad_SingleWildcardNoPrefix(t *testing.T) { // "core/*" should NOT match "other/php". assert.False(t, matchScope("core/*", "other/php")) } -func TestMatchScope_Bad_RecursiveWildcardNoPrefix(t *testing.T) { +func TestScope_MatchScope_Bad_RecursiveWildcardNoPrefix(t *testing.T) { assert.False(t, matchScope("core/**", "other/php")) } -func TestMatchScope_Bad_EmptyRepo(t *testing.T) { +func TestScope_MatchScope_Bad_EmptyRepo(t *testing.T) { assert.False(t, matchScope("core/*", "")) } -func TestMatchScope_Bad_WildcardInMiddle(t *testing.T) { +func TestScope_MatchScope_Bad_WildcardInMiddle(t *testing.T) { // Wildcard not at the end — should not match. assert.False(t, matchScope("core/*/sub", "core/php/sub")) } -func TestMatchScope_Bad_WildcardOnlyPrefix(t *testing.T) { +func TestScope_MatchScope_Bad_WildcardOnlyPrefix(t *testing.T) { // "core/*" should not match the prefix itself. assert.False(t, matchScope("core/*", "core")) assert.False(t, matchScope("core/*", "core/")) } -func TestMatchScope_Good_RecursiveWildcardSingleLevel(t *testing.T) { +func TestScope_MatchScope_Good_RecursiveWildcardSingleLevel(t *testing.T) { // "core/**" should also match single-level children. assert.True(t, matchScope("core/**", "core/php")) } -func TestMatchScope_Bad_RecursiveWildcardPrefixOnly(t *testing.T) { +func TestScope_MatchScope_Bad_RecursiveWildcardPrefixOnly(t *testing.T) { assert.False(t, matchScope("core/**", "core")) assert.False(t, matchScope("core/**", "corefoo")) } // --- repoAllowed with wildcards --- -func TestRepoAllowedWildcard_Good(t *testing.T) { +func TestScope_RepoAllowedWildcard_Good(t *testing.T) { scoped := []string{"core/*", "host-uk/docs"} assert.True(t, repoAllowed(scoped, "core/php")) assert.True(t, repoAllowed(scoped, "core/go-crypt")) assert.True(t, repoAllowed(scoped, "host-uk/docs")) } -func TestRepoAllowedWildcard_Good_Recursive(t *testing.T) { +func TestScope_RepoAllowedWildcard_Good_Recursive(t *testing.T) { scoped := []string{"core/**"} assert.True(t, repoAllowed(scoped, "core/php")) assert.True(t, repoAllowed(scoped, "core/php/sub")) } -func TestRepoAllowedWildcard_Bad_NoMatch(t *testing.T) { +func TestScope_RepoAllowedWildcard_Bad_NoMatch(t *testing.T) { scoped := []string{"core/*"} assert.False(t, repoAllowed(scoped, "other/repo")) assert.False(t, repoAllowed(scoped, "core/php/sub")) } -func TestRepoAllowedWildcard_Bad_EmptyRepo(t *testing.T) { +func TestScope_RepoAllowedWildcard_Bad_EmptyRepo(t *testing.T) { scoped := []string{"core/*"} assert.False(t, repoAllowed(scoped, "")) } -func TestRepoAllowedWildcard_Bad_EmptyScope(t *testing.T) { +func TestScope_RepoAllowedWildcard_Bad_EmptyScope(t *testing.T) { assert.False(t, repoAllowed(nil, "core/php")) assert.False(t, repoAllowed([]string{}, "core/php")) } // --- Integration: PolicyEngine with wildcard scopes --- -func TestEvaluateWildcardScope_Good_SingleLevel(t *testing.T) { +func TestScope_EvaluateWildcardScope_Good_SingleLevel(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "WildAgent", @@ -118,7 +118,7 @@ func TestEvaluateWildcardScope_Good_SingleLevel(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestEvaluateWildcardScope_Bad_OutOfScope(t *testing.T) { +func TestScope_EvaluateWildcardScope_Bad_OutOfScope(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "WildAgent", @@ -132,7 +132,7 @@ func TestEvaluateWildcardScope_Bad_OutOfScope(t *testing.T) { assert.Contains(t, result.Reason, "does not have access") } -func TestEvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) { +func TestScope_EvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "WildAgent", @@ -145,7 +145,7 @@ func TestEvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) { assert.Equal(t, Deny, result.Decision) } -func TestEvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) { +func TestScope_EvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "DeepAgent", @@ -158,7 +158,7 @@ func TestEvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestEvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) { +func TestScope_EvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "MixedAgent", @@ -180,7 +180,7 @@ func TestEvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) { assert.Equal(t, Deny, result.Decision) } -func TestEvaluateWildcardScope_Good_ReadSecretsScoped(t *testing.T) { +func TestScope_EvaluateWildcardScope_Good_ReadSecretsScoped(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "ScopedSecrets", diff --git a/trust/trust.go b/trust/trust.go index 8cc205f..6ed588e 100644 --- a/trust/trust.go +++ b/trust/trust.go @@ -20,18 +20,23 @@ import ( ) // Tier represents an agent's trust level in the system. +// Usage: use Tier with the other exported helpers in this package. type Tier int const ( // TierUntrusted is for external/community agents with minimal access. + // Usage: compare or pass TierUntrusted when using the related package APIs. TierUntrusted Tier = 1 // TierVerified is for partner agents with scoped access. + // Usage: compare or pass TierVerified when using the related package APIs. TierVerified Tier = 2 // TierFull is for internal agents with full access. + // Usage: compare or pass TierFull when using the related package APIs. TierFull Tier = 3 ) // String returns the human-readable name of the tier. +// Usage: call String(...) during the package's normal workflow. func (t Tier) String() string { switch t { case TierUntrusted: @@ -46,26 +51,47 @@ func (t Tier) String() string { } // Valid returns true if the tier is a recognised trust level. +// Usage: call Valid(...) during the package's normal workflow. func (t Tier) Valid() bool { return t >= TierUntrusted && t <= TierFull } // Capability represents a specific action an agent can perform. +// Usage: use Capability with the other exported helpers in this package. type Capability string const ( - CapPushRepo Capability = "repo.push" - CapMergePR Capability = "pr.merge" - CapCreatePR Capability = "pr.create" - CapCreateIssue Capability = "issue.create" - CapCommentIssue Capability = "issue.comment" - CapReadSecrets Capability = "secrets.read" - CapRunPrivileged Capability = "cmd.privileged" + // CapPushRepo allows pushing commits to a repository. + // Usage: pass CapPushRepo to PolicyEngine.Evaluate or include it in a Policy. + CapPushRepo Capability = "repo.push" + // CapMergePR allows merging a pull request. + // Usage: pass CapMergePR to PolicyEngine.Evaluate or include it in a Policy. + CapMergePR Capability = "pr.merge" + // CapCreatePR allows creating a pull request. + // Usage: pass CapCreatePR to PolicyEngine.Evaluate or include it in a Policy. + CapCreatePR Capability = "pr.create" + // CapCreateIssue allows creating an issue. + // Usage: pass CapCreateIssue to PolicyEngine.Evaluate or include it in a Policy. + CapCreateIssue Capability = "issue.create" + // CapCommentIssue allows commenting on an issue. + // Usage: pass CapCommentIssue to PolicyEngine.Evaluate or include it in a Policy. + CapCommentIssue Capability = "issue.comment" + // CapReadSecrets allows reading secret material. + // Usage: pass CapReadSecrets to PolicyEngine.Evaluate or include it in a Policy. + CapReadSecrets Capability = "secrets.read" + // CapRunPrivileged allows running privileged commands. + // Usage: pass CapRunPrivileged to PolicyEngine.Evaluate or include it in a Policy. + CapRunPrivileged Capability = "cmd.privileged" + // CapAccessWorkspace allows accessing the workspace filesystem. + // Usage: pass CapAccessWorkspace to PolicyEngine.Evaluate or include it in a Policy. CapAccessWorkspace Capability = "workspace.access" - CapModifyFlows Capability = "flows.modify" + // CapModifyFlows allows modifying workflow definitions. + // Usage: pass CapModifyFlows to PolicyEngine.Evaluate or include it in a Policy. + CapModifyFlows Capability = "flows.modify" ) // Agent represents an agent identity in the trust system. +// Usage: use Agent with the other exported helpers in this package. type Agent struct { // Name is the unique identifier for the agent (e.g., "Athena", "Clotho"). Name string @@ -83,12 +109,14 @@ type Agent struct { } // Registry manages agent identities and their trust tiers. +// Usage: use Registry with the other exported helpers in this package. type Registry struct { mu sync.RWMutex agents map[string]*Agent } // NewRegistry creates an empty agent registry. +// Usage: call NewRegistry(...) to create a ready-to-use value. func NewRegistry() *Registry { return &Registry{ agents: make(map[string]*Agent), @@ -97,6 +125,7 @@ func NewRegistry() *Registry { // Register adds or updates an agent in the registry. // Returns an error if the agent name is empty or the tier is invalid. +// Usage: call Register(...) during the package's normal workflow. func (r *Registry) Register(agent Agent) error { if agent.Name == "" { return coreerr.E("trust.Register", "agent name is required", nil) @@ -118,6 +147,7 @@ func (r *Registry) Register(agent Agent) error { } // Get returns the agent with the given name, or nil if not found. +// Usage: call Get(...) during the package's normal workflow. func (r *Registry) Get(name string) *Agent { r.mu.RLock() defer r.mu.RUnlock() @@ -125,6 +155,7 @@ func (r *Registry) Get(name string) *Agent { } // Remove deletes an agent from the registry. +// Usage: call Remove(...) during the package's normal workflow. func (r *Registry) Remove(name string) bool { r.mu.Lock() defer r.mu.Unlock() @@ -136,6 +167,7 @@ func (r *Registry) Remove(name string) bool { } // List returns all registered agents. The returned slice is a snapshot. +// Usage: call List(...) during the package's normal workflow. func (r *Registry) List() []Agent { r.mu.RLock() defer r.mu.RUnlock() @@ -147,6 +179,7 @@ func (r *Registry) List() []Agent { } // ListSeq returns an iterator over all registered agents. +// Usage: call ListSeq(...) during the package's normal workflow. func (r *Registry) ListSeq() iter.Seq[Agent] { return func(yield func(Agent) bool) { r.mu.RLock() @@ -160,6 +193,7 @@ func (r *Registry) ListSeq() iter.Seq[Agent] { } // Len returns the number of registered agents. +// Usage: call Len(...) during the package's normal workflow. func (r *Registry) Len() int { r.mu.RLock() defer r.mu.RUnlock() diff --git a/trust/trust_test.go b/trust/trust_test.go index 303facc..46b5d05 100644 --- a/trust/trust_test.go +++ b/trust/trust_test.go @@ -12,23 +12,23 @@ import ( // --- Tier --- -func TestTierString_Good(t *testing.T) { +func TestTrust_TierString_Good(t *testing.T) { assert.Equal(t, "untrusted", TierUntrusted.String()) assert.Equal(t, "verified", TierVerified.String()) assert.Equal(t, "full", TierFull.String()) } -func TestTierString_Bad_Unknown(t *testing.T) { +func TestTrust_TierString_Bad_Unknown(t *testing.T) { assert.Contains(t, Tier(99).String(), "unknown") } -func TestTierValid_Good(t *testing.T) { +func TestTrust_TierValid_Good(t *testing.T) { assert.True(t, TierUntrusted.Valid()) assert.True(t, TierVerified.Valid()) assert.True(t, TierFull.Valid()) } -func TestTierValid_Bad(t *testing.T) { +func TestTrust_TierValid_Bad(t *testing.T) { assert.False(t, Tier(0).Valid()) assert.False(t, Tier(4).Valid()) assert.False(t, Tier(-1).Valid()) @@ -36,14 +36,14 @@ func TestTierValid_Bad(t *testing.T) { // --- Registry --- -func TestRegistryRegister_Good(t *testing.T) { +func TestTrust_RegistryRegister_Good(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "Athena", Tier: TierFull}) require.NoError(t, err) assert.Equal(t, 1, r.Len()) } -func TestRegistryRegister_Good_SetsDefaults(t *testing.T) { +func TestTrust_RegistryRegister_Good_SetsDefaults(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "Athena", Tier: TierFull}) require.NoError(t, err) @@ -54,7 +54,7 @@ func TestRegistryRegister_Good_SetsDefaults(t *testing.T) { assert.False(t, a.CreatedAt.IsZero()) } -func TestRegistryRegister_Good_TierDefaults(t *testing.T) { +func TestTrust_RegistryRegister_Good_TierDefaults(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "A", Tier: TierUntrusted})) require.NoError(t, r.Register(Agent{Name: "B", Tier: TierVerified})) @@ -65,14 +65,14 @@ func TestRegistryRegister_Good_TierDefaults(t *testing.T) { assert.Equal(t, 0, r.Get("C").RateLimit) } -func TestRegistryRegister_Good_PreservesExplicitRateLimit(t *testing.T) { +func TestTrust_RegistryRegister_Good_PreservesExplicitRateLimit(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "Custom", Tier: TierVerified, RateLimit: 30}) require.NoError(t, err) assert.Equal(t, 30, r.Get("Custom").RateLimit) } -func TestRegistryRegister_Good_Update(t *testing.T) { +func TestTrust_RegistryRegister_Good_Update(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierVerified})) require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) @@ -81,21 +81,21 @@ func TestRegistryRegister_Good_Update(t *testing.T) { assert.Equal(t, TierFull, r.Get("Athena").Tier) } -func TestRegistryRegister_Bad_EmptyName(t *testing.T) { +func TestTrust_RegistryRegister_Bad_EmptyName(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Tier: TierFull}) assert.Error(t, err) assert.Contains(t, err.Error(), "name is required") } -func TestRegistryRegister_Bad_InvalidTier(t *testing.T) { +func TestTrust_RegistryRegister_Bad_InvalidTier(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "Bad", Tier: Tier(0)}) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -func TestRegistryGet_Good(t *testing.T) { +func TestTrust_RegistryGet_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) a := r.Get("Athena") @@ -103,24 +103,24 @@ func TestRegistryGet_Good(t *testing.T) { assert.Equal(t, "Athena", a.Name) } -func TestRegistryGet_Bad_NotFound(t *testing.T) { +func TestTrust_RegistryGet_Bad_NotFound(t *testing.T) { r := NewRegistry() assert.Nil(t, r.Get("nonexistent")) } -func TestRegistryRemove_Good(t *testing.T) { +func TestTrust_RegistryRemove_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) assert.True(t, r.Remove("Athena")) assert.Equal(t, 0, r.Len()) } -func TestRegistryRemove_Bad_NotFound(t *testing.T) { +func TestTrust_RegistryRemove_Bad_NotFound(t *testing.T) { r := NewRegistry() assert.False(t, r.Remove("nonexistent")) } -func TestRegistryList_Good(t *testing.T) { +func TestTrust_RegistryList_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) require.NoError(t, r.Register(Agent{Name: "Clotho", Tier: TierVerified})) @@ -136,12 +136,12 @@ func TestRegistryList_Good(t *testing.T) { assert.True(t, names["Clotho"]) } -func TestRegistryList_Good_Empty(t *testing.T) { +func TestTrust_RegistryList_Good_Empty(t *testing.T) { r := NewRegistry() assert.Empty(t, r.List()) } -func TestRegistryList_Good_Snapshot(t *testing.T) { +func TestTrust_RegistryList_Good_Snapshot(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) agents := r.List() @@ -151,7 +151,7 @@ func TestRegistryList_Good_Snapshot(t *testing.T) { assert.Equal(t, TierFull, r.Get("Athena").Tier) } -func TestRegistryListSeq_Good(t *testing.T) { +func TestTrust_RegistryListSeq_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) require.NoError(t, r.Register(Agent{Name: "Clotho", Tier: TierVerified})) @@ -169,7 +169,7 @@ func TestRegistryListSeq_Good(t *testing.T) { // --- Agent --- -func TestAgentTokenExpiry_Good(t *testing.T) { +func TestTrust_AgentTokenExpiry_Good(t *testing.T) { agent := Agent{ Name: "Test", Tier: TierVerified, @@ -183,9 +183,9 @@ func TestAgentTokenExpiry_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestConcurrentRegistryOperations_Good verifies that Register/Get/Remove +// TestTrust_ConcurrentRegistryOperations_Good verifies that Register/Get/Remove // from 10 goroutines do not race. -func TestConcurrentRegistryOperations_Good(t *testing.T) { +func TestTrust_ConcurrentRegistryOperations_Good(t *testing.T) { r := NewRegistry() const n = 10 @@ -224,24 +224,24 @@ func TestConcurrentRegistryOperations_Good(t *testing.T) { // No panic or data race = success (run with -race flag) } -// TestRegisterTierZero_Bad verifies that Tier 0 is rejected. -func TestRegisterTierZero_Bad(t *testing.T) { +// TestTrust_RegisterTierZero_Bad verifies that Tier 0 is rejected. +func TestTrust_RegisterTierZero_Bad(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "InvalidTierAgent", Tier: Tier(0)}) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -// TestRegisterNegativeTier_Bad verifies that negative tiers are rejected. -func TestRegisterNegativeTier_Bad(t *testing.T) { +// TestTrust_RegisterNegativeTier_Bad verifies that negative tiers are rejected. +func TestTrust_RegisterNegativeTier_Bad(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "NegativeTier", Tier: Tier(-1)}) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -// TestTokenExpiryBoundary_Good verifies token expiry checking. -func TestTokenExpiryBoundary_Good(t *testing.T) { +// TestTrust_TokenExpiryBoundary_Good verifies token expiry checking. +func TestTrust_TokenExpiryBoundary_Good(t *testing.T) { // Token that expires in the future — should be valid futureAgent := Agent{ Name: "FutureAgent", @@ -256,8 +256,8 @@ func TestTokenExpiryBoundary_Good(t *testing.T) { "token should now be expired") } -// TestTokenExpiryZeroValue_Ugly verifies zero-value TokenExpiresAt behaviour. -func TestTokenExpiryZeroValue_Ugly(t *testing.T) { +// TestTrust_TokenExpiryZeroValue_Ugly verifies zero-value TokenExpiresAt behaviour. +func TestTrust_TokenExpiryZeroValue_Ugly(t *testing.T) { agent := Agent{ Name: "ZeroExpiry", Tier: TierVerified, @@ -274,8 +274,8 @@ func TestTokenExpiryZeroValue_Ugly(t *testing.T) { "zero-value token expiry should be in the past") } -// TestConcurrentListDuringMutations_Good verifies List is safe during writes. -func TestConcurrentListDuringMutations_Good(t *testing.T) { +// TestTrust_ConcurrentListDuringMutations_Good verifies List is safe during writes. +func TestTrust_ConcurrentListDuringMutations_Good(t *testing.T) { r := NewRegistry() // Pre-populate -- 2.45.3 From a63b08156e19add56ef8b0182c6c341451251126 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 27 Mar 2026 03:11:30 +0000 Subject: [PATCH 6/9] chore: verification pass Co-Authored-By: Virgil -- 2.45.3 From 693964145e70e69610d1312aa91d44d5f5a7cac9 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 27 Mar 2026 04:46:56 +0000 Subject: [PATCH 7/9] test: enforce AX exact test naming Renames the remaining AX v0.8.0 test cases to the exact TestFile_Function_{Good,Bad,Ugly} pattern. Co-Authored-By: Virgil --- auth/auth_test.go | 16 +++---- auth/session_store_test.go | 38 ++++++++-------- cmd/testcmd/output_test.go | 2 +- crypt/chachapoly/chachapoly_test.go | 14 +++--- crypt/symmetric_test.go | 4 +- trust/approval_test.go | 30 ++++++------- trust/audit_test.go | 24 +++++----- trust/config_test.go | 26 +++++------ trust/policy_test.go | 70 ++++++++++++++--------------- trust/scope_test.go | 44 +++++++++--------- trust/trust_test.go | 22 ++++----- 11 files changed, 145 insertions(+), 145 deletions(-) diff --git a/auth/auth_test.go b/auth/auth_test.go index 822a571..8823632 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -1055,9 +1055,9 @@ func TestAuth_RotateKeyPair_Ugly(t *testing.T) { assert.Contains(t, err.Error(), "user not found") } -// TestAuth_RotateKeyPair_OldKeyCannotDecrypt_Good verifies old private key +// TestAuth_RotateKeyPairOldKeyCannotDecrypt_Good verifies old private key // cannot decrypt metadata after rotation. -func TestAuth_RotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) { +func TestAuth_RotateKeyPairOldKeyCannotDecrypt_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("rotate-crypto", "pass-a") @@ -1156,9 +1156,9 @@ func TestAuth_RevokeKey_Ugly(t *testing.T) { assert.Contains(t, err.Error(), "user not found") } -// TestAuth_IsRevoked_Placeholder_Good verifies that the legacy placeholder is not +// TestAuth_IsRevokedPlaceholder_Good verifies that the legacy placeholder is not // treated as a valid revocation. -func TestAuth_IsRevoked_Placeholder_Good(t *testing.T) { +func TestAuth_IsRevokedPlaceholder_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("placeholder-user", "pass") @@ -1174,16 +1174,16 @@ func TestAuth_IsRevoked_Placeholder_Good(t *testing.T) { assert.False(t, a.IsRevoked(userID)) } -// TestAuth_IsRevoked_NoRevFile_Good verifies that a missing .rev file returns false. -func TestAuth_IsRevoked_NoRevFile_Good(t *testing.T) { +// TestAuth_IsRevokedNoRevFile_Good verifies that a missing .rev file returns false. +func TestAuth_IsRevokedNoRevFile_Good(t *testing.T) { a, _ := newTestAuth() assert.False(t, a.IsRevoked("completely-nonexistent")) } -// TestAuth_RevokeKey_LegacyUser_Good verifies revocation works for a legacy user +// TestAuth_RevokeKeyLegacyUser_Good verifies revocation works for a legacy user // with only a .lthn hash file (no .hash file). -func TestAuth_RevokeKey_LegacyUser_Good(t *testing.T) { +func TestAuth_RevokeKeyLegacyUser_Good(t *testing.T) { m := io.NewMockMedium() a := New(m) diff --git a/auth/session_store_test.go b/auth/session_store_test.go index 896bdbf..d29b02b 100644 --- a/auth/session_store_test.go +++ b/auth/session_store_test.go @@ -16,7 +16,7 @@ import ( // --- MemorySessionStore --- -func TestSessionStore_MemorySessionStore_GetSetDelete_Good(t *testing.T) { +func TestSessionStore_MemorySessionStoreGetSetDelete_Good(t *testing.T) { store := NewMemorySessionStore() session := &Session{ @@ -44,21 +44,21 @@ func TestSessionStore_MemorySessionStore_GetSetDelete_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSessionStore_MemorySessionStore_GetNotFound_Bad(t *testing.T) { +func TestSessionStore_MemorySessionStoreGetNotFound_Bad(t *testing.T) { store := NewMemorySessionStore() _, err := store.Get("nonexistent-token") assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSessionStore_MemorySessionStore_DeleteNotFound_Bad(t *testing.T) { +func TestSessionStore_MemorySessionStoreDeleteNotFound_Bad(t *testing.T) { store := NewMemorySessionStore() err := store.Delete("nonexistent-token") assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSessionStore_MemorySessionStore_DeleteByUser_Good(t *testing.T) { +func TestSessionStore_MemorySessionStoreDeleteByUser_Good(t *testing.T) { store := NewMemorySessionStore() // Create sessions for two users @@ -94,7 +94,7 @@ func TestSessionStore_MemorySessionStore_DeleteByUser_Good(t *testing.T) { assert.Equal(t, "user-b", got.UserID) } -func TestSessionStore_MemorySessionStore_Cleanup_Good(t *testing.T) { +func TestSessionStore_MemorySessionStoreCleanup_Good(t *testing.T) { store := NewMemorySessionStore() // Create expired and valid sessions @@ -134,7 +134,7 @@ func TestSessionStore_MemorySessionStore_Cleanup_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSessionStore_MemorySessionStore_Concurrent_Good(t *testing.T) { +func TestSessionStore_MemorySessionStoreConcurrent_Good(t *testing.T) { store := NewMemorySessionStore() const n = 20 @@ -164,7 +164,7 @@ func TestSessionStore_MemorySessionStore_Concurrent_Good(t *testing.T) { // --- SQLiteSessionStore --- -func TestSessionStore_SQLiteSessionStore_GetSetDelete_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStoreGetSetDelete_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -194,7 +194,7 @@ func TestSessionStore_SQLiteSessionStore_GetSetDelete_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSessionStore_SQLiteSessionStore_GetNotFound_Bad(t *testing.T) { +func TestSessionStore_SQLiteSessionStoreGetNotFound_Bad(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -203,7 +203,7 @@ func TestSessionStore_SQLiteSessionStore_GetNotFound_Bad(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSessionStore_SQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) { +func TestSessionStore_SQLiteSessionStoreDeleteNotFound_Bad(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -212,7 +212,7 @@ func TestSessionStore_SQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSessionStore_SQLiteSessionStore_DeleteByUser_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStoreDeleteByUser_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -250,7 +250,7 @@ func TestSessionStore_SQLiteSessionStore_DeleteByUser_Good(t *testing.T) { assert.Equal(t, "user-b", got.UserID) } -func TestSessionStore_SQLiteSessionStore_Cleanup_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStoreCleanup_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -292,7 +292,7 @@ func TestSessionStore_SQLiteSessionStore_Cleanup_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSessionStore_SQLiteSessionStore_Persistence_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStorePersistence_Good(t *testing.T) { dir := t.TempDir() dbPath := core.Path(dir, "sessions.db") @@ -323,7 +323,7 @@ func TestSessionStore_SQLiteSessionStore_Persistence_Good(t *testing.T) { assert.Equal(t, "persist-token", got.Token) } -func TestSessionStore_SQLiteSessionStore_Concurrent_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStoreConcurrent_Good(t *testing.T) { // Use a temp file — :memory: SQLite has concurrency limitations dbPath := core.Path(t.TempDir(), "concurrent.db") store, err := NewSQLiteSessionStore(dbPath) @@ -359,7 +359,7 @@ func TestSessionStore_SQLiteSessionStore_Concurrent_Good(t *testing.T) { // --- Authenticator with SessionStore --- -func TestSessionStore_Authenticator_WithSessionStore_Good(t *testing.T) { +func TestSessionStore_AuthenticatorWithSessionStore_Good(t *testing.T) { sqliteStore, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer sqliteStore.Close() @@ -398,7 +398,7 @@ func TestSessionStore_Authenticator_WithSessionStore_Good(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestSessionStore_Authenticator_DefaultStore_Good(t *testing.T) { +func TestSessionStore_AuthenticatorDefaultStore_Good(t *testing.T) { m := io.NewMockMedium() a := New(m) @@ -407,7 +407,7 @@ func TestSessionStore_Authenticator_DefaultStore_Good(t *testing.T) { assert.True(t, ok, "default store should be MemorySessionStore") } -func TestSessionStore_Authenticator_StartCleanup_Good(t *testing.T) { +func TestSessionStore_AuthenticatorStartCleanup_Good(t *testing.T) { m := io.NewMockMedium() a := New(m, WithSessionTTL(1*time.Millisecond)) @@ -436,7 +436,7 @@ func TestSessionStore_Authenticator_StartCleanup_Good(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestSessionStore_Authenticator_StartCleanup_CancelStops_Good(t *testing.T) { +func TestSessionStore_AuthenticatorStartCleanupCancelStops_Good(t *testing.T) { m := io.NewMockMedium() a := New(m) @@ -448,7 +448,7 @@ func TestSessionStore_Authenticator_StartCleanup_CancelStops_Good(t *testing.T) time.Sleep(50 * time.Millisecond) } -func TestSessionStore_SQLiteSessionStore_UpdateExisting_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStoreUpdateExisting_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -476,7 +476,7 @@ func TestSessionStore_SQLiteSessionStore_UpdateExisting_Good(t *testing.T) { "updated session should have later expiry") } -func TestSessionStore_SQLiteSessionStore_TempFile_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStoreTempFile_Good(t *testing.T) { // Verify we can use a real temp file (not :memory:) tmpFile := core.Path(t.TempDir(), "go-crypt-test-session-store.db") diff --git a/cmd/testcmd/output_test.go b/cmd/testcmd/output_test.go index 80ed8b0..4131f10 100644 --- a/cmd/testcmd/output_test.go +++ b/cmd/testcmd/output_test.go @@ -33,7 +33,7 @@ FAIL dappco.re/go/core/pkg/bar assert.Equal(t, 50.0, results.packages[0].coverage) } -func TestOutput_PrintCoverageSummary_Good_LongPackageNames(t *testing.T) { +func TestOutput_PrintCoverageSummaryLongPackageNames_Good(t *testing.T) { // This tests the bug fix for long package names causing negative Repeat count results := testResults{ packages: []packageCoverage{ diff --git a/crypt/chachapoly/chachapoly_test.go b/crypt/chachapoly/chachapoly_test.go index 2c281db..6141258 100644 --- a/crypt/chachapoly/chachapoly_test.go +++ b/crypt/chachapoly/chachapoly_test.go @@ -31,14 +31,14 @@ func TestChachapoly_EncryptDecrypt_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestChachapoly_Encrypt_Bad_InvalidKeySize(t *testing.T) { +func TestChachapoly_EncryptInvalidKeySize_Bad(t *testing.T) { key := make([]byte, 16) // Wrong size plaintext := []byte("test") _, err := Encrypt(plaintext, key) assert.Error(t, err) } -func TestChachapoly_Decrypt_Bad_WrongKey(t *testing.T) { +func TestChachapoly_DecryptWrongKey_Bad(t *testing.T) { key1 := make([]byte, 32) key2 := make([]byte, 32) key2[0] = 1 // Different key @@ -51,7 +51,7 @@ func TestChachapoly_Decrypt_Bad_WrongKey(t *testing.T) { assert.Error(t, err) // Should fail authentication } -func TestChachapoly_Decrypt_Bad_TamperedCiphertext(t *testing.T) { +func TestChachapoly_DecryptTamperedCiphertext_Bad(t *testing.T) { key := make([]byte, 32) plaintext := []byte("secret") ciphertext, err := Encrypt(plaintext, key) @@ -64,7 +64,7 @@ func TestChachapoly_Decrypt_Bad_TamperedCiphertext(t *testing.T) { assert.Error(t, err) } -func TestChachapoly_Encrypt_Good_EmptyPlaintext(t *testing.T) { +func TestChachapoly_EncryptEmptyPlaintext_Good(t *testing.T) { key := make([]byte, 32) plaintext := []byte("") ciphertext, err := Encrypt(plaintext, key) @@ -76,7 +76,7 @@ func TestChachapoly_Encrypt_Good_EmptyPlaintext(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestChachapoly_Decrypt_Bad_ShortCiphertext(t *testing.T) { +func TestChachapoly_DecryptShortCiphertext_Bad(t *testing.T) { key := make([]byte, 32) shortCiphertext := []byte("short") @@ -93,7 +93,7 @@ func TestChachapoly_CiphertextDiffersFromPlaintext_Good(t *testing.T) { assert.NotEqual(t, plaintext, ciphertext) } -func TestChachapoly_Encrypt_Bad_NonceError(t *testing.T) { +func TestChachapoly_EncryptNonceError_Bad(t *testing.T) { key := make([]byte, 32) plaintext := []byte("test") @@ -106,7 +106,7 @@ func TestChachapoly_Encrypt_Bad_NonceError(t *testing.T) { assert.Error(t, err) } -func TestChachapoly_Decrypt_Bad_InvalidKeySize(t *testing.T) { +func TestChachapoly_DecryptInvalidKeySize_Bad(t *testing.T) { key := make([]byte, 16) // Wrong size ciphertext := []byte("test") _, err := Decrypt(ciphertext, key) diff --git a/crypt/symmetric_test.go b/crypt/symmetric_test.go index 46985f8..0888ba8 100644 --- a/crypt/symmetric_test.go +++ b/crypt/symmetric_test.go @@ -56,8 +56,8 @@ func TestSymmetric_AESGCM_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestSymmetric_AESGCM_Bad_WrongKey verifies wrong key returns error, not corrupt data. -func TestSymmetric_AESGCM_Bad_WrongKey(t *testing.T) { +// TestSymmetric_AESGCMWrongKey_Bad verifies wrong key returns error, not corrupt data. +func TestSymmetric_AESGCMWrongKey_Bad(t *testing.T) { key := make([]byte, 32) wrongKey := make([]byte, 32) _, _ = rand.Read(key) diff --git a/trust/approval_test.go b/trust/approval_test.go index f7bbee8..9dae0c0 100644 --- a/trust/approval_test.go +++ b/trust/approval_test.go @@ -17,7 +17,7 @@ func TestApproval_ApprovalStatusString_Good(t *testing.T) { assert.Equal(t, "denied", ApprovalDenied.String()) } -func TestApproval_ApprovalStatusString_Bad_Unknown(t *testing.T) { +func TestApproval_ApprovalStatusStringUnknown_Bad(t *testing.T) { assert.Contains(t, ApprovalStatus(99).String(), "unknown") } @@ -31,7 +31,7 @@ func TestApproval_ApprovalSubmit_Good(t *testing.T) { assert.Equal(t, 1, q.Len()) } -func TestApproval_ApprovalSubmit_Good_MultipleRequests(t *testing.T) { +func TestApproval_ApprovalSubmitMultipleRequests_Good(t *testing.T) { q := NewApprovalQueue() id1, err := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, err) @@ -42,7 +42,7 @@ func TestApproval_ApprovalSubmit_Good_MultipleRequests(t *testing.T) { assert.Equal(t, 2, q.Len()) } -func TestApproval_ApprovalSubmit_Good_EmptyRepo(t *testing.T) { +func TestApproval_ApprovalSubmitEmptyRepo_Good(t *testing.T) { q := NewApprovalQueue() id, err := q.Submit("Clotho", CapMergePR, "") require.NoError(t, err) @@ -53,14 +53,14 @@ func TestApproval_ApprovalSubmit_Good_EmptyRepo(t *testing.T) { assert.Empty(t, req.Repo) } -func TestApproval_ApprovalSubmit_Bad_EmptyAgent(t *testing.T) { +func TestApproval_ApprovalSubmitEmptyAgent_Bad(t *testing.T) { q := NewApprovalQueue() _, err := q.Submit("", CapMergePR, "") assert.Error(t, err) assert.Contains(t, err.Error(), "agent name is required") } -func TestApproval_ApprovalSubmit_Bad_EmptyCapability(t *testing.T) { +func TestApproval_ApprovalSubmitEmptyCapability_Bad(t *testing.T) { q := NewApprovalQueue() _, err := q.Submit("Clotho", "", "") assert.Error(t, err) @@ -85,7 +85,7 @@ func TestApproval_ApprovalGet_Good(t *testing.T) { assert.True(t, req.ReviewedAt.IsZero()) } -func TestApproval_ApprovalGet_Good_ReturnsSnapshot(t *testing.T) { +func TestApproval_ApprovalGetReturnsSnapshot_Good(t *testing.T) { q := NewApprovalQueue() id, err := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, err) @@ -99,7 +99,7 @@ func TestApproval_ApprovalGet_Good_ReturnsSnapshot(t *testing.T) { assert.Equal(t, ApprovalPending, original.Status) } -func TestApproval_ApprovalGet_Bad_NotFound(t *testing.T) { +func TestApproval_ApprovalGetNotFound_Bad(t *testing.T) { q := NewApprovalQueue() assert.Nil(t, q.Get("nonexistent")) } @@ -121,14 +121,14 @@ func TestApproval_ApprovalApprove_Good(t *testing.T) { assert.False(t, req.ReviewedAt.IsZero()) } -func TestApproval_ApprovalApprove_Bad_NotFound(t *testing.T) { +func TestApproval_ApprovalApproveNotFound_Bad(t *testing.T) { q := NewApprovalQueue() err := q.Approve("nonexistent", "admin", "") assert.Error(t, err) assert.Contains(t, err.Error(), "not found") } -func TestApproval_ApprovalApprove_Bad_AlreadyApproved(t *testing.T) { +func TestApproval_ApprovalApproveAlreadyApproved_Bad(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, q.Approve(id, "admin", "")) @@ -138,7 +138,7 @@ func TestApproval_ApprovalApprove_Bad_AlreadyApproved(t *testing.T) { assert.Contains(t, err.Error(), "already approved") } -func TestApproval_ApprovalApprove_Bad_AlreadyDenied(t *testing.T) { +func TestApproval_ApprovalApproveAlreadyDenied_Bad(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, q.Deny(id, "admin", "nope")) @@ -165,14 +165,14 @@ func TestApproval_ApprovalDeny_Good(t *testing.T) { assert.False(t, req.ReviewedAt.IsZero()) } -func TestApproval_ApprovalDeny_Bad_NotFound(t *testing.T) { +func TestApproval_ApprovalDenyNotFound_Bad(t *testing.T) { q := NewApprovalQueue() err := q.Deny("nonexistent", "admin", "") assert.Error(t, err) assert.Contains(t, err.Error(), "not found") } -func TestApproval_ApprovalDeny_Bad_AlreadyDenied(t *testing.T) { +func TestApproval_ApprovalDenyAlreadyDenied_Bad(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, q.Deny(id, "admin", "")) @@ -196,7 +196,7 @@ func TestApproval_ApprovalPending_Good(t *testing.T) { assert.Len(t, pending, 2) } -func TestApproval_ApprovalPending_Good_Empty(t *testing.T) { +func TestApproval_ApprovalPendingEmpty_Good(t *testing.T) { q := NewApprovalQueue() assert.Empty(t, q.Pending()) } @@ -270,7 +270,7 @@ func TestApproval_ApprovalConcurrent_Good(t *testing.T) { // --- Integration: PolicyEngine + ApprovalQueue --- -func TestApproval_ApprovalWorkflow_Good_EndToEnd(t *testing.T) { +func TestApproval_ApprovalWorkflowEndToEnd_Good(t *testing.T) { pe := newTestEngine(t) q := NewApprovalQueue() @@ -293,7 +293,7 @@ func TestApproval_ApprovalWorkflow_Good_EndToEnd(t *testing.T) { assert.Equal(t, "Virgil", req.ReviewedBy) } -func TestApproval_ApprovalWorkflow_Good_DenyEndToEnd(t *testing.T) { +func TestApproval_ApprovalWorkflowDenyEndToEnd_Good(t *testing.T) { pe := newTestEngine(t) q := NewApprovalQueue() diff --git a/trust/audit_test.go b/trust/audit_test.go index 583a461..c7c80f5 100644 --- a/trust/audit_test.go +++ b/trust/audit_test.go @@ -26,7 +26,7 @@ func TestAudit_AuditRecord_Good(t *testing.T) { assert.Equal(t, 1, log.Len()) } -func TestAudit_AuditRecord_Good_EntryFields(t *testing.T) { +func TestAudit_AuditRecordEntryFields_Good(t *testing.T) { log := NewAuditLog(nil) result := EvalResult{ @@ -50,7 +50,7 @@ func TestAudit_AuditRecord_Good_EntryFields(t *testing.T) { assert.False(t, e.Timestamp.IsZero()) } -func TestAudit_AuditRecord_Good_NoRepo(t *testing.T) { +func TestAudit_AuditRecordNoRepo_Good(t *testing.T) { log := NewAuditLog(nil) result := EvalResult{ Decision: Allow, @@ -66,7 +66,7 @@ func TestAudit_AuditRecord_Good_NoRepo(t *testing.T) { assert.Empty(t, entries[0].Repo) } -func TestAudit_AuditEntries_Good_Snapshot(t *testing.T) { +func TestAudit_AuditEntriesSnapshot_Good(t *testing.T) { log := NewAuditLog(nil) log.Record(EvalResult{Agent: "A", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "") @@ -78,12 +78,12 @@ func TestAudit_AuditEntries_Good_Snapshot(t *testing.T) { assert.Equal(t, "A", log.Entries()[0].Agent) } -func TestAudit_AuditEntries_Good_Empty(t *testing.T) { +func TestAudit_AuditEntriesEmpty_Good(t *testing.T) { log := NewAuditLog(nil) assert.Empty(t, log.Entries()) } -func TestAudit_AuditEntries_Good_AppendOnly(t *testing.T) { +func TestAudit_AuditEntriesAppendOnly_Good(t *testing.T) { log := NewAuditLog(nil) for i := range 5 { @@ -133,7 +133,7 @@ func TestAudit_AuditEntriesSeq_Good(t *testing.T) { assert.Equal(t, 2, count) } -func TestAudit_AuditEntriesFor_Bad_NotFound(t *testing.T) { +func TestAudit_AuditEntriesForNotFound_Bad(t *testing.T) { log := NewAuditLog(nil) log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "") @@ -142,7 +142,7 @@ func TestAudit_AuditEntriesFor_Bad_NotFound(t *testing.T) { // --- Writer output --- -func TestAudit_AuditRecord_Good_WritesToWriter(t *testing.T) { +func TestAudit_AuditRecordWritesToWriter_Good(t *testing.T) { buf := core.NewBuilder() log := NewAuditLog(buf) @@ -168,7 +168,7 @@ func TestAudit_AuditRecord_Good_WritesToWriter(t *testing.T) { assert.Equal(t, "host-uk/core", entry.Repo) } -func TestAudit_AuditRecord_Good_MultipleLines(t *testing.T) { +func TestAudit_AuditRecordMultipleLines_Good(t *testing.T) { buf := core.NewBuilder() log := NewAuditLog(buf) @@ -192,7 +192,7 @@ func TestAudit_AuditRecord_Good_MultipleLines(t *testing.T) { } } -func TestAudit_AuditRecord_Bad_WriterError(t *testing.T) { +func TestAudit_AuditRecordWriterError_Bad(t *testing.T) { log := NewAuditLog(&failWriter{}) result := EvalResult{ @@ -218,7 +218,7 @@ func (f *failWriter) Write(_ []byte) (int, error) { // --- Decision JSON marshalling --- -func TestAudit_DecisionJSON_Good_RoundTrip(t *testing.T) { +func TestAudit_DecisionJSONRoundTrip_Good(t *testing.T) { decisions := []Decision{Deny, Allow, NeedsApproval} expected := []string{`"deny"`, `"allow"`, `"needs_approval"`} @@ -234,7 +234,7 @@ func TestAudit_DecisionJSON_Good_RoundTrip(t *testing.T) { } } -func TestAudit_DecisionJSON_Bad_UnknownString(t *testing.T) { +func TestAudit_DecisionJSONUnknownString_Bad(t *testing.T) { var d Decision result := core.JSONUnmarshal([]byte(`"invalid"`), &d) err, _ := result.Value.(error) @@ -242,7 +242,7 @@ func TestAudit_DecisionJSON_Bad_UnknownString(t *testing.T) { assert.Contains(t, err.Error(), "unknown decision") } -func TestAudit_DecisionJSON_Bad_NonString(t *testing.T) { +func TestAudit_DecisionJSONNonString_Bad(t *testing.T) { var d Decision result := core.JSONUnmarshal([]byte(`42`), &d) err, _ := result.Value.(error) diff --git a/trust/config_test.go b/trust/config_test.go index b4a0e01..2165349 100644 --- a/trust/config_test.go +++ b/trust/config_test.go @@ -36,7 +36,7 @@ func TestConfig_LoadPolicies_Good(t *testing.T) { assert.Len(t, policies, 3) } -func TestConfig_LoadPolicies_Good_FieldMapping(t *testing.T) { +func TestConfig_LoadPoliciesFieldMapping_Good(t *testing.T) { policies, err := LoadPolicies(core.NewReader(validPolicyJSON)) require.NoError(t, err) @@ -60,33 +60,33 @@ func TestConfig_LoadPolicies_Good_FieldMapping(t *testing.T) { assert.Len(t, policies[2].Denied, 2) } -func TestConfig_LoadPolicies_Good_EmptyPolicies(t *testing.T) { +func TestConfig_LoadPoliciesEmptyPolicies_Good(t *testing.T) { input := `{"policies": []}` policies, err := LoadPolicies(core.NewReader(input)) require.NoError(t, err) assert.Empty(t, policies) } -func TestConfig_LoadPolicies_Bad_InvalidJSON(t *testing.T) { +func TestConfig_LoadPoliciesInvalidJSON_Bad(t *testing.T) { _, err := LoadPolicies(core.NewReader(`{invalid`)) assert.Error(t, err) } -func TestConfig_LoadPolicies_Bad_InvalidTier(t *testing.T) { +func TestConfig_LoadPoliciesInvalidTier_Bad(t *testing.T) { input := `{"policies": [{"tier": 0, "allowed": ["repo.push"]}]}` _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -func TestConfig_LoadPolicies_Bad_TierTooHigh(t *testing.T) { +func TestConfig_LoadPoliciesTierTooHigh_Bad(t *testing.T) { input := `{"policies": [{"tier": 99, "allowed": ["repo.push"]}]}` _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -func TestConfig_LoadPolicies_Bad_UnknownField(t *testing.T) { +func TestConfig_LoadPoliciesUnknownField_Bad(t *testing.T) { input := `{"policies": [{"tier": 1, "allowed": ["repo.push"], "bogus": true}]}` _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err, "DisallowUnknownFields should reject unknown fields") @@ -104,7 +104,7 @@ func TestConfig_LoadPoliciesFromFile_Good(t *testing.T) { assert.Len(t, policies, 3) } -func TestConfig_LoadPoliciesFromFile_Bad_NotFound(t *testing.T) { +func TestConfig_LoadPoliciesFromFileNotFound_Bad(t *testing.T) { _, err := LoadPoliciesFromFile("/nonexistent/path/policies.json") assert.Error(t, err) } @@ -135,7 +135,7 @@ func TestConfig_ApplyPolicies_Good(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestConfig_ApplyPolicies_Bad_InvalidJSON(t *testing.T) { +func TestConfig_ApplyPoliciesInvalidJSON_Bad(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) @@ -143,7 +143,7 @@ func TestConfig_ApplyPolicies_Bad_InvalidJSON(t *testing.T) { assert.Error(t, err) } -func TestConfig_ApplyPolicies_Bad_InvalidTier(t *testing.T) { +func TestConfig_ApplyPoliciesInvalidTier_Bad(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) @@ -172,7 +172,7 @@ func TestConfig_ApplyPoliciesFromFile_Good(t *testing.T) { assert.Len(t, p.Allowed, 3) } -func TestConfig_ApplyPoliciesFromFile_Bad_NotFound(t *testing.T) { +func TestConfig_ApplyPoliciesFromFileNotFound_Bad(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) err := pe.ApplyPoliciesFromFile("/nonexistent/policies.json") @@ -196,7 +196,7 @@ func TestConfig_ExportPolicies_Good(t *testing.T) { assert.Len(t, cfg.Policies, 3) } -func TestConfig_ExportPolicies_Good_RoundTrip(t *testing.T) { +func TestConfig_ExportPoliciesRoundTrip_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "A", Tier: TierFull})) pe := NewPolicyEngine(r) @@ -239,7 +239,7 @@ func TestConfig_ToCapabilities_Good(t *testing.T) { assert.Equal(t, CapMergePR, caps[1]) } -func TestConfig_ToCapabilities_Good_Empty(t *testing.T) { +func TestConfig_ToCapabilitiesEmpty_Good(t *testing.T) { assert.Nil(t, toCapabilities(nil)) assert.Nil(t, toCapabilities([]string{})) } @@ -251,7 +251,7 @@ func TestConfig_FromCapabilities_Good(t *testing.T) { assert.Equal(t, "pr.merge", ss[1]) } -func TestConfig_FromCapabilities_Good_Empty(t *testing.T) { +func TestConfig_FromCapabilitiesEmpty_Good(t *testing.T) { assert.Nil(t, fromCapabilities(nil)) assert.Nil(t, fromCapabilities([]Capability{})) } diff --git a/trust/policy_test.go b/trust/policy_test.go index 19ff5f7..413a100 100644 --- a/trust/policy_test.go +++ b/trust/policy_test.go @@ -35,13 +35,13 @@ func TestPolicy_DecisionString_Good(t *testing.T) { assert.Equal(t, "needs_approval", NeedsApproval.String()) } -func TestPolicy_DecisionString_Bad_Unknown(t *testing.T) { +func TestPolicy_DecisionStringUnknown_Bad(t *testing.T) { assert.Contains(t, Decision(99).String(), "unknown") } // --- Tier 3 (Full Trust) --- -func TestPolicy_Evaluate_Good_Tier3CanDoAnything(t *testing.T) { +func TestPolicy_EvaluateTier3CanDoAnything_Good(t *testing.T) { pe := newTestEngine(t) caps := []Capability{ @@ -57,56 +57,56 @@ func TestPolicy_Evaluate_Good_Tier3CanDoAnything(t *testing.T) { // --- Tier 2 (Verified) --- -func TestPolicy_Evaluate_Good_Tier2CanCreatePR(t *testing.T) { +func TestPolicy_EvaluateTier2CanCreatePR_Good(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapCreatePR, "host-uk/core") assert.Equal(t, Allow, result.Decision) } -func TestPolicy_Evaluate_Good_Tier2CanPushToScopedRepo(t *testing.T) { +func TestPolicy_EvaluateTier2CanPushToScopedRepo_Good(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapPushRepo, "host-uk/core") assert.Equal(t, Allow, result.Decision) } -func TestPolicy_Evaluate_Good_Tier2NeedsApprovalToMerge(t *testing.T) { +func TestPolicy_EvaluateTier2NeedsApprovalToMerge_Good(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapMergePR, "host-uk/core") assert.Equal(t, NeedsApproval, result.Decision) } -func TestPolicy_Evaluate_Good_Tier2CanCreateIssue(t *testing.T) { +func TestPolicy_EvaluateTier2CanCreateIssue_Good(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapCreateIssue, "") assert.Equal(t, Allow, result.Decision) } -func TestPolicy_Evaluate_Bad_Tier2CannotAccessWorkspace(t *testing.T) { +func TestPolicy_EvaluateTier2CannotAccessWorkspace_Bad(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapAccessWorkspace, "") assert.Equal(t, Deny, result.Decision) } -func TestPolicy_Evaluate_Bad_Tier2CannotModifyFlows(t *testing.T) { +func TestPolicy_EvaluateTier2CannotModifyFlows_Bad(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapModifyFlows, "") assert.Equal(t, Deny, result.Decision) } -func TestPolicy_Evaluate_Bad_Tier2CannotRunPrivileged(t *testing.T) { +func TestPolicy_EvaluateTier2CannotRunPrivileged_Bad(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapRunPrivileged, "") assert.Equal(t, Deny, result.Decision) } -func TestPolicy_Evaluate_Bad_Tier2CannotPushToUnscopedRepo(t *testing.T) { +func TestPolicy_EvaluateTier2CannotPushToUnscopedRepo_Bad(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapPushRepo, "host-uk/secret-repo") assert.Equal(t, Deny, result.Decision) assert.Contains(t, result.Reason, "does not have access") } -func TestPolicy_Evaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) { +func TestPolicy_EvaluateTier2RepoScopeEmptyRepo_Bad(t *testing.T) { pe := newTestEngine(t) // Push without specifying a repo should be denied for scoped agents. result := pe.Evaluate("Clotho", CapPushRepo, "") @@ -115,43 +115,43 @@ func TestPolicy_Evaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) { // --- Tier 1 (Untrusted) --- -func TestPolicy_Evaluate_Good_Tier1CanCreatePR(t *testing.T) { +func TestPolicy_EvaluateTier1CanCreatePR_Good(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapCreatePR, "") assert.Equal(t, Allow, result.Decision) } -func TestPolicy_Evaluate_Good_Tier1CanCommentIssue(t *testing.T) { +func TestPolicy_EvaluateTier1CanCommentIssue_Good(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapCommentIssue, "") assert.Equal(t, Allow, result.Decision) } -func TestPolicy_Evaluate_Bad_Tier1CannotPush(t *testing.T) { +func TestPolicy_EvaluateTier1CannotPush_Bad(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapPushRepo, "") assert.Equal(t, Deny, result.Decision) } -func TestPolicy_Evaluate_Bad_Tier1CannotMerge(t *testing.T) { +func TestPolicy_EvaluateTier1CannotMerge_Bad(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapMergePR, "") assert.Equal(t, Deny, result.Decision) } -func TestPolicy_Evaluate_Bad_Tier1CannotCreateIssue(t *testing.T) { +func TestPolicy_EvaluateTier1CannotCreateIssue_Bad(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapCreateIssue, "") assert.Equal(t, Deny, result.Decision) } -func TestPolicy_Evaluate_Bad_Tier1CannotReadSecrets(t *testing.T) { +func TestPolicy_EvaluateTier1CannotReadSecrets_Bad(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapReadSecrets, "") assert.Equal(t, Deny, result.Decision) } -func TestPolicy_Evaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) { +func TestPolicy_EvaluateTier1CannotRunPrivileged_Bad(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapRunPrivileged, "") assert.Equal(t, Deny, result.Decision) @@ -159,14 +159,14 @@ func TestPolicy_Evaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) { // --- Edge cases --- -func TestPolicy_Evaluate_Bad_UnknownAgent(t *testing.T) { +func TestPolicy_EvaluateUnknownAgent_Bad(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Unknown", CapCreatePR, "") assert.Equal(t, Deny, result.Decision) assert.Contains(t, result.Reason, "not registered") } -func TestPolicy_Evaluate_Good_EvalResultFields(t *testing.T) { +func TestPolicy_EvaluateEvalResultFields_Good(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Athena", CapPushRepo, "") assert.Equal(t, "Athena", result.Agent) @@ -189,7 +189,7 @@ func TestPolicy_SetPolicy_Good(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestPolicy_SetPolicy_Bad_InvalidTier(t *testing.T) { +func TestPolicy_SetPolicyInvalidTier_Bad(t *testing.T) { pe := newTestEngine(t) err := pe.SetPolicy(Policy{Tier: Tier(0)}) assert.Error(t, err) @@ -203,7 +203,7 @@ func TestPolicy_GetPolicy_Good(t *testing.T) { assert.Equal(t, TierFull, p.Tier) } -func TestPolicy_GetPolicy_Bad_NotFound(t *testing.T) { +func TestPolicy_GetPolicyNotFound_Bad(t *testing.T) { pe := newTestEngine(t) assert.Nil(t, pe.GetPolicy(Tier(99))) } @@ -217,7 +217,7 @@ func TestPolicy_IsRepoScoped_Good(t *testing.T) { assert.True(t, isRepoScoped(CapReadSecrets)) } -func TestPolicy_IsRepoScoped_Bad_NotScoped(t *testing.T) { +func TestPolicy_IsRepoScopedNotScoped_Bad(t *testing.T) { assert.False(t, isRepoScoped(CapRunPrivileged)) assert.False(t, isRepoScoped(CapAccessWorkspace)) assert.False(t, isRepoScoped(CapModifyFlows)) @@ -229,24 +229,24 @@ func TestPolicy_RepoAllowed_Good(t *testing.T) { assert.True(t, repoAllowed(scoped, "host-uk/docs")) } -func TestPolicy_RepoAllowed_Bad_NotInScope(t *testing.T) { +func TestPolicy_RepoAllowedNotInScope_Bad(t *testing.T) { scoped := []string{"host-uk/core"} assert.False(t, repoAllowed(scoped, "host-uk/secret")) } -func TestPolicy_RepoAllowed_Bad_EmptyRepo(t *testing.T) { +func TestPolicy_RepoAllowedEmptyRepo_Bad(t *testing.T) { scoped := []string{"host-uk/core"} assert.False(t, repoAllowed(scoped, "")) } -func TestPolicy_RepoAllowed_Bad_EmptyScope(t *testing.T) { +func TestPolicy_RepoAllowedEmptyScope_Bad(t *testing.T) { assert.False(t, repoAllowed(nil, "host-uk/core")) assert.False(t, repoAllowed([]string{}, "host-uk/core")) } // --- Tier 3 ignores repo scoping --- -func TestPolicy_Evaluate_Good_Tier3IgnoresRepoScope(t *testing.T) { +func TestPolicy_EvaluateTier3IgnoresRepoScope_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "Virgil", @@ -270,11 +270,11 @@ func TestPolicy_DefaultRateLimit_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestPolicy_Evaluate_Good_Tier2EmptyScopedReposAllowsAll verifies that a Tier 2 +// TestPolicy_EvaluateTier2EmptyScopedReposAllowsAll_Good verifies that a Tier 2 // agent with empty ScopedRepos is treated as "unrestricted" for repo-scoped // capabilities. NOTE: This is a potential security concern documented in // FINDINGS.md — empty ScopedRepos bypasses the repo scope check entirely. -func TestPolicy_Evaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) { +func TestPolicy_EvaluateTier2EmptyScopedReposAllowsAll_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "Hypnos", @@ -301,9 +301,9 @@ func TestPolicy_Evaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -// TestPolicy_Evaluate_Bad_CapabilityNotInAnyList verifies that a capability not in +// TestPolicy_EvaluateCapabilityNotInAnyList_Bad verifies that a capability not in // allowed, denied, or requires_approval lists defaults to deny. -func TestPolicy_Evaluate_Bad_CapabilityNotInAnyList(t *testing.T) { +func TestPolicy_EvaluateCapabilityNotInAnyList_Bad(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "TestAgent", @@ -325,9 +325,9 @@ func TestPolicy_Evaluate_Bad_CapabilityNotInAnyList(t *testing.T) { assert.Contains(t, result.Reason, "not granted") } -// TestPolicy_Evaluate_Bad_UnknownCapability verifies that a completely invented +// TestPolicy_EvaluateUnknownCapability_Bad verifies that a completely invented // capability string is denied. -func TestPolicy_Evaluate_Bad_UnknownCapability(t *testing.T) { +func TestPolicy_EvaluateUnknownCapability_Bad(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Athena", Capability("nonexistent.capability"), "") @@ -360,10 +360,10 @@ func TestPolicy_ConcurrentEvaluate_Good(t *testing.T) { wg.Wait() } -// TestPolicy_Evaluate_Bad_Tier2ScopedReposWithEmptyRepoParam verifies that +// TestPolicy_EvaluateTier2ScopedReposWithEmptyRepoParam_Bad verifies that // a scoped agent requesting a repo-scoped capability without specifying // the repo is denied. -func TestPolicy_Evaluate_Bad_Tier2ScopedReposWithEmptyRepoParam(t *testing.T) { +func TestPolicy_EvaluateTier2ScopedReposWithEmptyRepoParam_Bad(t *testing.T) { pe := newTestEngine(t) // Clotho has ScopedRepos but passes empty repo diff --git a/trust/scope_test.go b/trust/scope_test.go index d2de46d..1f64fbf 100644 --- a/trust/scope_test.go +++ b/trust/scope_test.go @@ -9,62 +9,62 @@ import ( // --- matchScope --- -func TestScope_MatchScope_Good_ExactMatch(t *testing.T) { +func TestScope_MatchScopeExactMatch_Good(t *testing.T) { assert.True(t, matchScope("host-uk/core", "host-uk/core")) } -func TestScope_MatchScope_Good_SingleWildcard(t *testing.T) { +func TestScope_MatchScopeSingleWildcard_Good(t *testing.T) { assert.True(t, matchScope("core/*", "core/php")) assert.True(t, matchScope("core/*", "core/go-crypt")) assert.True(t, matchScope("host-uk/*", "host-uk/core")) } -func TestScope_MatchScope_Good_RecursiveWildcard(t *testing.T) { +func TestScope_MatchScopeRecursiveWildcard_Good(t *testing.T) { assert.True(t, matchScope("core/**", "core/php")) assert.True(t, matchScope("core/**", "core/php/sub")) assert.True(t, matchScope("core/**", "core/a/b/c")) } -func TestScope_MatchScope_Bad_ExactMismatch(t *testing.T) { +func TestScope_MatchScopeExactMismatch_Bad(t *testing.T) { assert.False(t, matchScope("host-uk/core", "host-uk/docs")) } -func TestScope_MatchScope_Bad_SingleWildcardNoNested(t *testing.T) { +func TestScope_MatchScopeSingleWildcardNoNested_Bad(t *testing.T) { // "core/*" should NOT match "core/php/sub" — only single level. assert.False(t, matchScope("core/*", "core/php/sub")) assert.False(t, matchScope("core/*", "core/a/b")) } -func TestScope_MatchScope_Bad_SingleWildcardNoPrefix(t *testing.T) { +func TestScope_MatchScopeSingleWildcardNoPrefix_Bad(t *testing.T) { // "core/*" should NOT match "other/php". assert.False(t, matchScope("core/*", "other/php")) } -func TestScope_MatchScope_Bad_RecursiveWildcardNoPrefix(t *testing.T) { +func TestScope_MatchScopeRecursiveWildcardNoPrefix_Bad(t *testing.T) { assert.False(t, matchScope("core/**", "other/php")) } -func TestScope_MatchScope_Bad_EmptyRepo(t *testing.T) { +func TestScope_MatchScopeEmptyRepo_Bad(t *testing.T) { assert.False(t, matchScope("core/*", "")) } -func TestScope_MatchScope_Bad_WildcardInMiddle(t *testing.T) { +func TestScope_MatchScopeWildcardInMiddle_Bad(t *testing.T) { // Wildcard not at the end — should not match. assert.False(t, matchScope("core/*/sub", "core/php/sub")) } -func TestScope_MatchScope_Bad_WildcardOnlyPrefix(t *testing.T) { +func TestScope_MatchScopeWildcardOnlyPrefix_Bad(t *testing.T) { // "core/*" should not match the prefix itself. assert.False(t, matchScope("core/*", "core")) assert.False(t, matchScope("core/*", "core/")) } -func TestScope_MatchScope_Good_RecursiveWildcardSingleLevel(t *testing.T) { +func TestScope_MatchScopeRecursiveWildcardSingleLevel_Good(t *testing.T) { // "core/**" should also match single-level children. assert.True(t, matchScope("core/**", "core/php")) } -func TestScope_MatchScope_Bad_RecursiveWildcardPrefixOnly(t *testing.T) { +func TestScope_MatchScopeRecursiveWildcardPrefixOnly_Bad(t *testing.T) { assert.False(t, matchScope("core/**", "core")) assert.False(t, matchScope("core/**", "corefoo")) } @@ -78,31 +78,31 @@ func TestScope_RepoAllowedWildcard_Good(t *testing.T) { assert.True(t, repoAllowed(scoped, "host-uk/docs")) } -func TestScope_RepoAllowedWildcard_Good_Recursive(t *testing.T) { +func TestScope_RepoAllowedWildcardRecursive_Good(t *testing.T) { scoped := []string{"core/**"} assert.True(t, repoAllowed(scoped, "core/php")) assert.True(t, repoAllowed(scoped, "core/php/sub")) } -func TestScope_RepoAllowedWildcard_Bad_NoMatch(t *testing.T) { +func TestScope_RepoAllowedWildcardNoMatch_Bad(t *testing.T) { scoped := []string{"core/*"} assert.False(t, repoAllowed(scoped, "other/repo")) assert.False(t, repoAllowed(scoped, "core/php/sub")) } -func TestScope_RepoAllowedWildcard_Bad_EmptyRepo(t *testing.T) { +func TestScope_RepoAllowedWildcardEmptyRepo_Bad(t *testing.T) { scoped := []string{"core/*"} assert.False(t, repoAllowed(scoped, "")) } -func TestScope_RepoAllowedWildcard_Bad_EmptyScope(t *testing.T) { +func TestScope_RepoAllowedWildcardEmptyScope_Bad(t *testing.T) { assert.False(t, repoAllowed(nil, "core/php")) assert.False(t, repoAllowed([]string{}, "core/php")) } // --- Integration: PolicyEngine with wildcard scopes --- -func TestScope_EvaluateWildcardScope_Good_SingleLevel(t *testing.T) { +func TestScope_EvaluateWildcardScopeSingleLevel_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "WildAgent", @@ -118,7 +118,7 @@ func TestScope_EvaluateWildcardScope_Good_SingleLevel(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestScope_EvaluateWildcardScope_Bad_OutOfScope(t *testing.T) { +func TestScope_EvaluateWildcardScopeOutOfScope_Bad(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "WildAgent", @@ -132,7 +132,7 @@ func TestScope_EvaluateWildcardScope_Bad_OutOfScope(t *testing.T) { assert.Contains(t, result.Reason, "does not have access") } -func TestScope_EvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) { +func TestScope_EvaluateWildcardScopeNestedNotAllowedBySingleStar_Bad(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "WildAgent", @@ -145,7 +145,7 @@ func TestScope_EvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing assert.Equal(t, Deny, result.Decision) } -func TestScope_EvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) { +func TestScope_EvaluateWildcardScopeRecursiveAllowsNested_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "DeepAgent", @@ -158,7 +158,7 @@ func TestScope_EvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestScope_EvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) { +func TestScope_EvaluateWildcardScopeMixedExactAndWildcard_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "MixedAgent", @@ -180,7 +180,7 @@ func TestScope_EvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) { assert.Equal(t, Deny, result.Decision) } -func TestScope_EvaluateWildcardScope_Good_ReadSecretsScoped(t *testing.T) { +func TestScope_EvaluateWildcardScopeReadSecretsScoped_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "ScopedSecrets", diff --git a/trust/trust_test.go b/trust/trust_test.go index 46b5d05..c2cd9d4 100644 --- a/trust/trust_test.go +++ b/trust/trust_test.go @@ -18,7 +18,7 @@ func TestTrust_TierString_Good(t *testing.T) { assert.Equal(t, "full", TierFull.String()) } -func TestTrust_TierString_Bad_Unknown(t *testing.T) { +func TestTrust_TierStringUnknown_Bad(t *testing.T) { assert.Contains(t, Tier(99).String(), "unknown") } @@ -43,7 +43,7 @@ func TestTrust_RegistryRegister_Good(t *testing.T) { assert.Equal(t, 1, r.Len()) } -func TestTrust_RegistryRegister_Good_SetsDefaults(t *testing.T) { +func TestTrust_RegistryRegisterSetsDefaults_Good(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "Athena", Tier: TierFull}) require.NoError(t, err) @@ -54,7 +54,7 @@ func TestTrust_RegistryRegister_Good_SetsDefaults(t *testing.T) { assert.False(t, a.CreatedAt.IsZero()) } -func TestTrust_RegistryRegister_Good_TierDefaults(t *testing.T) { +func TestTrust_RegistryRegisterTierDefaults_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "A", Tier: TierUntrusted})) require.NoError(t, r.Register(Agent{Name: "B", Tier: TierVerified})) @@ -65,14 +65,14 @@ func TestTrust_RegistryRegister_Good_TierDefaults(t *testing.T) { assert.Equal(t, 0, r.Get("C").RateLimit) } -func TestTrust_RegistryRegister_Good_PreservesExplicitRateLimit(t *testing.T) { +func TestTrust_RegistryRegisterPreservesExplicitRateLimit_Good(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "Custom", Tier: TierVerified, RateLimit: 30}) require.NoError(t, err) assert.Equal(t, 30, r.Get("Custom").RateLimit) } -func TestTrust_RegistryRegister_Good_Update(t *testing.T) { +func TestTrust_RegistryRegisterUpdate_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierVerified})) require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) @@ -81,14 +81,14 @@ func TestTrust_RegistryRegister_Good_Update(t *testing.T) { assert.Equal(t, TierFull, r.Get("Athena").Tier) } -func TestTrust_RegistryRegister_Bad_EmptyName(t *testing.T) { +func TestTrust_RegistryRegisterEmptyName_Bad(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Tier: TierFull}) assert.Error(t, err) assert.Contains(t, err.Error(), "name is required") } -func TestTrust_RegistryRegister_Bad_InvalidTier(t *testing.T) { +func TestTrust_RegistryRegisterInvalidTier_Bad(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "Bad", Tier: Tier(0)}) assert.Error(t, err) @@ -103,7 +103,7 @@ func TestTrust_RegistryGet_Good(t *testing.T) { assert.Equal(t, "Athena", a.Name) } -func TestTrust_RegistryGet_Bad_NotFound(t *testing.T) { +func TestTrust_RegistryGetNotFound_Bad(t *testing.T) { r := NewRegistry() assert.Nil(t, r.Get("nonexistent")) } @@ -115,7 +115,7 @@ func TestTrust_RegistryRemove_Good(t *testing.T) { assert.Equal(t, 0, r.Len()) } -func TestTrust_RegistryRemove_Bad_NotFound(t *testing.T) { +func TestTrust_RegistryRemoveNotFound_Bad(t *testing.T) { r := NewRegistry() assert.False(t, r.Remove("nonexistent")) } @@ -136,12 +136,12 @@ func TestTrust_RegistryList_Good(t *testing.T) { assert.True(t, names["Clotho"]) } -func TestTrust_RegistryList_Good_Empty(t *testing.T) { +func TestTrust_RegistryListEmpty_Good(t *testing.T) { r := NewRegistry() assert.Empty(t, r.List()) } -func TestTrust_RegistryList_Good_Snapshot(t *testing.T) { +func TestTrust_RegistryListSnapshot_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) agents := r.List() -- 2.45.3 From 60b713114bc911012521ad7db8bae4eccc46f9c5 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 27 Mar 2026 19:30:16 +0000 Subject: [PATCH 8/9] docs: populate package specs Generate specs for every Go package by reading the current source tree and documenting exported types, functions, and methods in a mirrored specs/ layout. Co-Authored-By: Virgil --- specs/auth/README.md | 521 +++++++++++++++++++++++++++ specs/cmd/crypt/README.md | 20 ++ specs/cmd/testcmd/README.md | 20 ++ specs/crypt/README.md | 250 +++++++++++++ specs/crypt/chachapoly/README.md | 30 ++ specs/crypt/lthn/README.md | 63 ++++ specs/crypt/openpgp/README.md | 69 ++++ specs/crypt/pgp/README.md | 77 ++++ specs/crypt/rsa/README.md | 56 +++ specs/trust/README.md | 599 +++++++++++++++++++++++++++++++ 10 files changed, 1705 insertions(+) create mode 100644 specs/auth/README.md create mode 100644 specs/cmd/crypt/README.md create mode 100644 specs/cmd/testcmd/README.md create mode 100644 specs/crypt/README.md create mode 100644 specs/crypt/chachapoly/README.md create mode 100644 specs/crypt/lthn/README.md create mode 100644 specs/crypt/openpgp/README.md create mode 100644 specs/crypt/pgp/README.md create mode 100644 specs/crypt/rsa/README.md create mode 100644 specs/trust/README.md diff --git a/specs/auth/README.md b/specs/auth/README.md new file mode 100644 index 0000000..0ee6a29 --- /dev/null +++ b/specs/auth/README.md @@ -0,0 +1,521 @@ +# auth + +**Import:** `dappco.re/go/core/crypt/auth` + +**Files:** 4 + +## Types + +### `Authenticator` + +```go +type Authenticator struct { + medium io.Medium + store SessionStore + hardwareKey HardwareKey // optional hardware key (nil = software only) + challenges map[string]*Challenge // userID -> pending challenge + mu sync.RWMutex // protects challenges map only + challengeTTL time.Duration + sessionTTL time.Duration +} +``` + +Authenticator manages PGP-based challenge-response authentication. +All user data and keys are persisted through an io.Medium, which may +be backed by disk, memory (MockMedium), or any other storage backend. +Sessions are persisted via a SessionStore (in-memory by default, +optionally SQLite-backed for crash recovery). + +An optional HardwareKey can be provided via WithHardwareKey for +hardware-backed cryptographic operations (PKCS#11, YubiKey, etc.). +See auth/hardware.go for the interface definition and integration points. +Usage: create an Authenticator with New(...) and then call Register, Login, or CreateChallenge. + +#### Methods + +##### `CreateChallenge` + +```go +func (a *Authenticator) CreateChallenge(userID string) (*Challenge, error) +``` + +CreateChallenge generates a cryptographic challenge for the given user. +A random nonce is created and encrypted with the user's PGP public key. +The client must decrypt the nonce and sign it to prove key ownership. +Usage: call CreateChallenge(...) during the package's normal workflow. + +##### `DeleteUser` + +```go +func (a *Authenticator) DeleteUser(userID string) error +``` + +DeleteUser removes a user and all associated keys from storage. +The "server" user is protected and cannot be deleted (mirroring the +original TypeScript implementation's safeguard). +Usage: call DeleteUser(...) during the package's normal workflow. + +##### `IsRevoked` + +```go +func (a *Authenticator) IsRevoked(userID string) bool +``` + +IsRevoked checks whether a user's key has been revoked by inspecting the +.rev file. Returns true only if the file contains valid revocation JSON +(not the legacy "REVOCATION_PLACEHOLDER" string). +Usage: call IsRevoked(...) during the package's normal workflow. + +##### `Login` + +```go +func (a *Authenticator) Login(userID, password string) (*Session, error) +``` + +Login performs password-based authentication as a convenience method. +It verifies the password against the stored hash and, on success, +creates a new session. This bypasses the PGP challenge-response flow. + +Hash format detection: + - If a .hash file exists, its content starts with "$argon2id$" and is verified + using constant-time Argon2id comparison. + - Otherwise, falls back to legacy .lthn file with LTHN hash verification. + On successful legacy login, the password is re-hashed with Argon2id and + a .hash file is written (transparent migration). + +Usage: call Login(...) for password-based flows when challenge-response is not required. + +##### `ReadResponseFile` + +```go +func (a *Authenticator) ReadResponseFile(userID, path string) (*Session, error) +``` + +ReadResponseFile reads a signed response from a file and validates it, +completing the air-gapped authentication flow. The file must contain the +raw PGP signature bytes (armored). +Usage: call ReadResponseFile(...) during the package's normal workflow. + +##### `RefreshSession` + +```go +func (a *Authenticator) RefreshSession(token string) (*Session, error) +``` + +RefreshSession extends the expiry of an existing valid session. +Usage: call RefreshSession(...) during the package's normal workflow. + +##### `Register` + +```go +func (a *Authenticator) Register(username, password string) (*User, error) +``` + +Register creates a new user account. It hashes the username with LTHN to +produce a userID, generates a PGP keypair (protected by the given password), +and persists the public key, private key, revocation placeholder, password +hash (Argon2id), and encrypted metadata via the Medium. +Usage: call Register(...) during the package's normal workflow. + +##### `RevokeKey` + +```go +func (a *Authenticator) RevokeKey(userID, password, reason string) error +``` + +RevokeKey marks a user's key as revoked. It verifies the password first, +writes a JSON revocation record to the .rev file (replacing the placeholder), +and invalidates all sessions for the user. +Usage: call RevokeKey(...) during the package's normal workflow. + +##### `RevokeSession` + +```go +func (a *Authenticator) RevokeSession(token string) error +``` + +RevokeSession removes a session, invalidating the token immediately. +Usage: call RevokeSession(...) during the package's normal workflow. + +##### `RotateKeyPair` + +```go +func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) (*User, error) +``` + +RotateKeyPair generates a new PGP keypair for the given user, re-encrypts +their metadata with the new key, updates the password hash, and invalidates +all existing sessions. The caller must provide the current password +(oldPassword) to decrypt existing metadata and the new password (newPassword) +to protect the new keypair. +Usage: call RotateKeyPair(...) during the package's normal workflow. + +##### `StartCleanup` + +```go +func (a *Authenticator) StartCleanup(ctx context.Context, interval time.Duration) +``` + +StartCleanup runs a background goroutine that periodically removes expired +sessions from the store. It stops when the context is cancelled. +Usage: call StartCleanup(...) during the package's normal workflow. + +##### `ValidateResponse` + +```go +func (a *Authenticator) ValidateResponse(userID string, signedNonce []byte) (*Session, error) +``` + +ValidateResponse verifies a signed nonce from the client. The client must +have decrypted the challenge nonce and signed it with their private key. +On success, a new session is created and returned. +Usage: call ValidateResponse(...) during the package's normal workflow. + +##### `ValidateSession` + +```go +func (a *Authenticator) ValidateSession(token string) (*Session, error) +``` + +ValidateSession checks whether a token maps to a valid, non-expired session. +Usage: call ValidateSession(...) during the package's normal workflow. + +##### `WriteChallengeFile` + +```go +func (a *Authenticator) WriteChallengeFile(userID, path string) error +``` + +WriteChallengeFile writes an encrypted challenge to a file for air-gapped +(courier) transport. The challenge is created and then its encrypted nonce +is written to the specified path on the Medium. +Usage: call WriteChallengeFile(...) during the package's normal workflow. + + +### `Challenge` + +```go +type Challenge struct { + Nonce []byte `json:"nonce"` + Encrypted string `json:"encrypted"` // PGP-encrypted nonce (armored) + ExpiresAt time.Time `json:"expires_at"` +} +``` + +Challenge is a PGP-encrypted nonce sent to a client during authentication. +Usage: use Challenge with the other exported helpers in this package. + + +### `HardwareKey` + +```go +type HardwareKey interface { + // Sign produces a cryptographic signature over the given data using the + // hardware-stored private key. The signature format depends on the + // underlying device (e.g. ECDSA, RSA-PSS, EdDSA). + Sign(data []byte) ([]byte, error) + + // Decrypt decrypts ciphertext using the hardware-stored private key. + // The ciphertext format must match what the device expects (e.g. RSA-OAEP). + Decrypt(ciphertext []byte) ([]byte, error) + + // GetPublicKey returns the PEM or armored public key corresponding to the + // hardware-stored private key. + GetPublicKey() (string, error) + + // IsAvailable reports whether the hardware key device is currently + // connected and operational. Callers should check this before attempting + // Sign or Decrypt to provide graceful fallback behaviour. + IsAvailable() bool +} +``` + +HardwareKey defines the contract for hardware-backed cryptographic operations. +Implementations should wrap PKCS#11 tokens, YubiKeys, TPM modules, or +similar tamper-resistant devices. + +All methods must be safe for concurrent use. +Usage: implement HardwareKey and pass it to WithHardwareKey(...) to wire hardware-backed auth into New(...). + + +### `MemorySessionStore` + +```go +type MemorySessionStore struct { + mu sync.RWMutex + sessions map[string]*Session +} +``` + +MemorySessionStore is an in-memory SessionStore backed by a map. +Usage: use MemorySessionStore with the other exported helpers in this package. + +#### Methods + +##### `Cleanup` + +```go +func (m *MemorySessionStore) Cleanup() (int, error) +``` + +Cleanup removes all expired sessions and returns the count removed. +Usage: call Cleanup(...) during the package's normal workflow. + +##### `Delete` + +```go +func (m *MemorySessionStore) Delete(token string) error +``` + +Delete removes a session by token. +Usage: call Delete(...) during the package's normal workflow. + +##### `DeleteByUser` + +```go +func (m *MemorySessionStore) DeleteByUser(userID string) error +``` + +DeleteByUser removes all sessions belonging to the given user. +Usage: call DeleteByUser(...) during the package's normal workflow. + +##### `Get` + +```go +func (m *MemorySessionStore) Get(token string) (*Session, error) +``` + +Get retrieves a session by token. +Usage: call Get(...) during the package's normal workflow. + +##### `Set` + +```go +func (m *MemorySessionStore) Set(session *Session) error +``` + +Set stores a session, keyed by its token. +Usage: call Set(...) during the package's normal workflow. + + +### `Option` + +```go +type Option func(*Authenticator) +``` + +Option configures an Authenticator. +Usage: use Option with the other exported helpers in this package. + + +### `Revocation` + +```go +type Revocation struct { + UserID string `json:"user_id"` + Reason string `json:"reason"` + RevokedAt time.Time `json:"revoked_at"` +} +``` + +Revocation records the details of a revoked user key. +Stored as JSON in the user's .rev file, replacing the legacy placeholder. +Usage: use Revocation with the other exported helpers in this package. + + +### `SQLiteSessionStore` + +```go +type SQLiteSessionStore struct { + mu sync.Mutex + store *store.Store +} +``` + +SQLiteSessionStore is a SessionStore backed by core/store (SQLite KV). +A mutex serialises all operations because SQLite is single-writer. +Usage: use SQLiteSessionStore with the other exported helpers in this package. + +#### Methods + +##### `Cleanup` + +```go +func (s *SQLiteSessionStore) Cleanup() (int, error) +``` + +Cleanup removes all expired sessions and returns the count removed. +Usage: call Cleanup(...) during the package's normal workflow. + +##### `Close` + +```go +func (s *SQLiteSessionStore) Close() error +``` + +Close closes the underlying SQLite store. +Usage: call Close(...) during the package's normal workflow. + +##### `Delete` + +```go +func (s *SQLiteSessionStore) Delete(token string) error +``` + +Delete removes a session by token from SQLite. +Usage: call Delete(...) during the package's normal workflow. + +##### `DeleteByUser` + +```go +func (s *SQLiteSessionStore) DeleteByUser(userID string) error +``` + +DeleteByUser removes all sessions belonging to the given user. +Usage: call DeleteByUser(...) during the package's normal workflow. + +##### `Get` + +```go +func (s *SQLiteSessionStore) Get(token string) (*Session, error) +``` + +Get retrieves a session by token from SQLite. +Usage: call Get(...) during the package's normal workflow. + +##### `Set` + +```go +func (s *SQLiteSessionStore) Set(session *Session) error +``` + +Set stores a session in SQLite, keyed by its token. +Usage: call Set(...) during the package's normal workflow. + + +### `Session` + +```go +type Session struct { + Token string `json:"token"` + UserID string `json:"user_id"` + ExpiresAt time.Time `json:"expires_at"` +} +``` + +Session represents an authenticated session. +Usage: use Session with the other exported helpers in this package. + + +### `SessionStore` + +```go +type SessionStore interface { + Get(token string) (*Session, error) + Set(session *Session) error + Delete(token string) error + DeleteByUser(userID string) error + Cleanup() (int, error) // Remove expired sessions, return count removed +} +``` + +SessionStore abstracts session persistence. +Usage: use SessionStore with the other exported helpers in this package. + + +### `User` + +```go +type User struct { + PublicKey string `json:"public_key"` + KeyID string `json:"key_id"` + Fingerprint string `json:"fingerprint"` + PasswordHash string `json:"password_hash"` // Argon2id (new) or LTHN (legacy) + Created time.Time `json:"created"` + LastLogin time.Time `json:"last_login"` +} +``` + +User represents a registered user with PGP credentials. +Usage: use User with the other exported helpers in this package. + +## Functions + +### `New` + +```go +func New(m io.Medium, opts ...Option) *Authenticator +``` + +New creates an Authenticator that persists user data via the given Medium. +By default, sessions are stored in memory. Use WithSessionStore to provide +a persistent implementation (e.g. SQLiteSessionStore). +Usage: call New(...) to create a ready-to-use value. + + +### `NewMemorySessionStore` + +```go +func NewMemorySessionStore() *MemorySessionStore +``` + +NewMemorySessionStore creates a new in-memory session store. +Usage: call NewMemorySessionStore(...) to create a ready-to-use value. + + +### `NewSQLiteSessionStore` + +```go +func NewSQLiteSessionStore(dbPath string) (*SQLiteSessionStore, error) +``` + +NewSQLiteSessionStore creates a new SQLite-backed session store. +Use ":memory:" for testing or a file path for persistent storage. +Usage: call NewSQLiteSessionStore(...) to create a ready-to-use value. + + +### `WithChallengeTTL` + +```go +func WithChallengeTTL(d time.Duration) Option +``` + +WithChallengeTTL sets the lifetime of a challenge before it expires. +Usage: pass WithChallengeTTL(...) into the related constructor to adjust the default behaviour. + + +### `WithHardwareKey` + +```go +func WithHardwareKey(hk HardwareKey) Option +``` + +WithHardwareKey configures the Authenticator to use a hardware key for +cryptographic operations where supported. When set, the Authenticator may +delegate signing, decryption, and public key retrieval to the hardware +device instead of using software PGP keys. + +This is a forward-looking option — integration points are documented in +auth.go but not yet wired up. +Usage: pass WithHardwareKey(...) into New(...) to enable a HardwareKey implementation. + + +### `WithSessionStore` + +```go +func WithSessionStore(s SessionStore) Option +``` + +WithSessionStore sets the SessionStore implementation. +If not provided, an in-memory store is used (sessions lost on restart). +Usage: pass WithSessionStore(...) into the related constructor to adjust the default behaviour. + + +### `WithSessionTTL` + +```go +func WithSessionTTL(d time.Duration) Option +``` + +WithSessionTTL sets the lifetime of a session before it expires. +Usage: pass WithSessionTTL(...) into the related constructor to adjust the default behaviour. \ No newline at end of file diff --git a/specs/cmd/crypt/README.md b/specs/cmd/crypt/README.md new file mode 100644 index 0000000..6a28c64 --- /dev/null +++ b/specs/cmd/crypt/README.md @@ -0,0 +1,20 @@ +# crypt + +**Import:** `dappco.re/go/core/crypt/cmd/crypt` + +**Files:** 5 + +## Types + +None. + +## Functions + +### `AddCryptCommands` + +```go +func AddCryptCommands(root *cli.Command) +``` + +AddCryptCommands registers the 'crypt' command group and all subcommands. +Usage: call AddCryptCommands(...) during the package's normal workflow. \ No newline at end of file diff --git a/specs/cmd/testcmd/README.md b/specs/cmd/testcmd/README.md new file mode 100644 index 0000000..3963d38 --- /dev/null +++ b/specs/cmd/testcmd/README.md @@ -0,0 +1,20 @@ +# testcmd + +**Import:** `dappco.re/go/core/crypt/cmd/testcmd` + +**Files:** 4 + +## Types + +None. + +## Functions + +### `AddTestCommands` + +```go +func AddTestCommands(root *cli.Command) +``` + +AddTestCommands registers the 'test' command and all subcommands. +Usage: call AddTestCommands(...) during the package's normal workflow. \ No newline at end of file diff --git a/specs/crypt/README.md b/specs/crypt/README.md new file mode 100644 index 0000000..254737c --- /dev/null +++ b/specs/crypt/README.md @@ -0,0 +1,250 @@ +# crypt + +**Import:** `dappco.re/go/core/crypt/crypt` + +**Files:** 6 + +## Types + +None. + +## Functions + +### `AESGCMDecrypt` + +```go +func AESGCMDecrypt(ciphertext, key []byte) ([]byte, error) +``` + +AESGCMDecrypt decrypts ciphertext encrypted with AESGCMEncrypt. +The key must be 32 bytes. Expects the nonce prepended to the ciphertext. +Usage: call AESGCMDecrypt(...) during the package's normal workflow. + + +### `AESGCMEncrypt` + +```go +func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) +``` + +AESGCMEncrypt encrypts plaintext using AES-256-GCM. +The key must be 32 bytes. The nonce is randomly generated and prepended +to the ciphertext. +Usage: call AESGCMEncrypt(...) during the package's normal workflow. + + +### `ChaCha20Decrypt` + +```go +func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) +``` + +ChaCha20Decrypt decrypts ciphertext encrypted with ChaCha20Encrypt. +The key must be 32 bytes. Expects the nonce prepended to the ciphertext. +Usage: call ChaCha20Decrypt(...) during the package's normal workflow. + + +### `ChaCha20Encrypt` + +```go +func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) +``` + +ChaCha20Encrypt encrypts plaintext using ChaCha20-Poly1305. +The key must be 32 bytes. The nonce is randomly generated and prepended +to the ciphertext. +Usage: call ChaCha20Encrypt(...) during the package's normal workflow. + + +### `Decrypt` + +```go +func Decrypt(ciphertext, passphrase []byte) ([]byte, error) +``` + +Decrypt decrypts data encrypted with Encrypt. +Expects format: salt (16 bytes) + nonce (24 bytes) + ciphertext. +Usage: call Decrypt(...) during the package's normal workflow. + + +### `DecryptAES` + +```go +func DecryptAES(ciphertext, passphrase []byte) ([]byte, error) +``` + +DecryptAES decrypts data encrypted with EncryptAES. +Expects format: salt (16 bytes) + nonce (12 bytes) + ciphertext. +Usage: call DecryptAES(...) during the package's normal workflow. + + +### `DeriveKey` + +```go +func DeriveKey(passphrase, salt []byte, keyLen uint32) []byte +``` + +DeriveKey derives a key from a passphrase using Argon2id with default parameters. +The salt must be argon2SaltLen bytes. keyLen specifies the desired key length. +Usage: call DeriveKey(...) during the package's normal workflow. + + +### `DeriveKeyScrypt` + +```go +func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error) +``` + +DeriveKeyScrypt derives a key from a passphrase using scrypt. +Uses recommended parameters: N=32768, r=8, p=1. +Usage: call DeriveKeyScrypt(...) during the package's normal workflow. + + +### `Encrypt` + +```go +func Encrypt(plaintext, passphrase []byte) ([]byte, error) +``` + +Encrypt encrypts data with a passphrase using ChaCha20-Poly1305. +A random salt is generated and prepended to the output. +Format: salt (16 bytes) + nonce (24 bytes) + ciphertext. +Usage: call Encrypt(...) during the package's normal workflow. + + +### `EncryptAES` + +```go +func EncryptAES(plaintext, passphrase []byte) ([]byte, error) +``` + +EncryptAES encrypts data using AES-256-GCM with a passphrase. +A random salt is generated and prepended to the output. +Format: salt (16 bytes) + nonce (12 bytes) + ciphertext. +Usage: call EncryptAES(...) during the package's normal workflow. + + +### `HKDF` + +```go +func HKDF(secret, salt, info []byte, keyLen int) ([]byte, error) +``` + +HKDF derives a key using HKDF-SHA256. +secret is the input keying material, salt is optional (can be nil), +info is optional context, and keyLen is the desired output length. +Usage: call HKDF(...) during the package's normal workflow. + + +### `HMACSHA256` + +```go +func HMACSHA256(message, key []byte) []byte +``` + +HMACSHA256 computes the HMAC-SHA256 of a message using the given key. +Usage: call HMACSHA256(...) during the package's normal workflow. + + +### `HMACSHA512` + +```go +func HMACSHA512(message, key []byte) []byte +``` + +HMACSHA512 computes the HMAC-SHA512 of a message using the given key. +Usage: call HMACSHA512(...) during the package's normal workflow. + + +### `HashBcrypt` + +```go +func HashBcrypt(password string, cost int) (string, error) +``` + +HashBcrypt hashes a password using bcrypt with the given cost. +Cost must be between bcrypt.MinCost and bcrypt.MaxCost. +Usage: call HashBcrypt(...) during the package's normal workflow. + + +### `HashPassword` + +```go +func HashPassword(password string) (string, error) +``` + +HashPassword hashes a password using Argon2id with default parameters. +Returns a string in the format: $argon2id$v=19$m=65536,t=3,p=4$$ +Usage: call HashPassword(...) during the package's normal workflow. + + +### `SHA256File` + +```go +func SHA256File(path string) (string, error) +``` + +SHA256File computes the SHA-256 checksum of a file and returns it as a hex string. +Usage: call SHA256File(...) during the package's normal workflow. + + +### `SHA256Sum` + +```go +func SHA256Sum(data []byte) string +``` + +SHA256Sum computes the SHA-256 checksum of data and returns it as a hex string. +Usage: call SHA256Sum(...) during the package's normal workflow. + + +### `SHA512File` + +```go +func SHA512File(path string) (string, error) +``` + +SHA512File computes the SHA-512 checksum of a file and returns it as a hex string. +Usage: call SHA512File(...) during the package's normal workflow. + + +### `SHA512Sum` + +```go +func SHA512Sum(data []byte) string +``` + +SHA512Sum computes the SHA-512 checksum of data and returns it as a hex string. +Usage: call SHA512Sum(...) during the package's normal workflow. + + +### `VerifyBcrypt` + +```go +func VerifyBcrypt(password, hash string) (bool, error) +``` + +VerifyBcrypt verifies a password against a bcrypt hash. +Usage: call VerifyBcrypt(...) during the package's normal workflow. + + +### `VerifyHMAC` + +```go +func VerifyHMAC(message, key, mac []byte, hashFunc func() hash.Hash) bool +``` + +VerifyHMAC verifies an HMAC using constant-time comparison. +hashFunc should be sha256.New, sha512.New, etc. +Usage: call VerifyHMAC(...) during the package's normal workflow. + + +### `VerifyPassword` + +```go +func VerifyPassword(password, hash string) (bool, error) +``` + +VerifyPassword verifies a password against an Argon2id hash string. +The hash must be in the format produced by HashPassword. +Usage: call VerifyPassword(...) during the package's normal workflow. \ No newline at end of file diff --git a/specs/crypt/chachapoly/README.md b/specs/crypt/chachapoly/README.md new file mode 100644 index 0000000..8f081c9 --- /dev/null +++ b/specs/crypt/chachapoly/README.md @@ -0,0 +1,30 @@ +# chachapoly + +**Import:** `dappco.re/go/core/crypt/crypt/chachapoly` + +**Files:** 1 + +## Types + +None. + +## Functions + +### `Decrypt` + +```go +func Decrypt(ciphertext []byte, key []byte) ([]byte, error) +``` + +Decrypt decrypts data using ChaCha20-Poly1305. +Usage: call Decrypt(...) during the package's normal workflow. + + +### `Encrypt` + +```go +func Encrypt(plaintext []byte, key []byte) ([]byte, error) +``` + +Encrypt encrypts data using ChaCha20-Poly1305. +Usage: call Encrypt(...) during the package's normal workflow. \ No newline at end of file diff --git a/specs/crypt/lthn/README.md b/specs/crypt/lthn/README.md new file mode 100644 index 0000000..63516f0 --- /dev/null +++ b/specs/crypt/lthn/README.md @@ -0,0 +1,63 @@ +# lthn + +**Import:** `dappco.re/go/core/crypt/crypt/lthn` + +**Files:** 1 + +## Types + +None. + +## Functions + +### `GetKeyMap` + +```go +func GetKeyMap() map[rune]rune +``` + +GetKeyMap returns the current character substitution map. +Usage: call GetKeyMap(...) during the package's normal workflow. + + +### `Hash` + +```go +func Hash(input string) string +``` + +Hash computes the LTHN hash of the input string. + +The algorithm: + 1. Derive a quasi-salt by reversing the input and applying character substitutions + 2. Concatenate: input + salt + 3. Compute SHA-256 of the concatenated string + 4. Return the hex-encoded digest (64 characters, lowercase) + +The same input always produces the same hash, enabling verification +without storing a separate salt value. +Usage: call Hash(...) when you need a deterministic content-style digest rather than a password hash. + + +### `SetKeyMap` + +```go +func SetKeyMap(newKeyMap map[rune]rune) +``` + +SetKeyMap replaces the default character substitution map. +Use this to customize the quasi-salt derivation for specific applications. +Changes affect all subsequent Hash and Verify calls. +Usage: call SetKeyMap(...) during the package's normal workflow. + + +### `Verify` + +```go +func Verify(input string, hash string) bool +``` + +Verify checks if an input string produces the given hash. +Returns true if Hash(input) equals the provided hash value. +Uses constant-time comparison to prevent timing attacks. +Usage: call Verify(...) during the package's normal workflow. \ No newline at end of file diff --git a/specs/crypt/openpgp/README.md b/specs/crypt/openpgp/README.md new file mode 100644 index 0000000..a327f34 --- /dev/null +++ b/specs/crypt/openpgp/README.md @@ -0,0 +1,69 @@ +# openpgp + +**Import:** `dappco.re/go/core/crypt/crypt/openpgp` + +**Files:** 1 + +## Types + +### `Service` + +```go +type Service struct { + core *framework.Core +} +``` + +Service provides OpenPGP cryptographic operations. +Usage: use Service with the other exported helpers in this package. + +#### Methods + +##### `CreateKeyPair` + +```go +func (s *Service) CreateKeyPair(name, passphrase string) (string, error) +``` + +CreateKeyPair generates a new RSA-4096 PGP keypair. +Returns the armored private key string. +Usage: call CreateKeyPair(...) during the package's normal workflow. + +##### `DecryptPGP` + +```go +func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any) (string, error) +``` + +DecryptPGP decrypts a PGP message using the provided armored private key and passphrase. +Usage: call DecryptPGP(...) during the package's normal workflow. + +##### `EncryptPGP` + +```go +func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opts ...any) (string, error) +``` + +EncryptPGP encrypts data for a recipient identified by their public key (armored string in recipientPath). +The encrypted data is written to the provided writer and also returned as an armored string. +Usage: call EncryptPGP(...) during the package's normal workflow. + +##### `HandleIPCEvents` + +```go +func (s *Service) HandleIPCEvents(c *framework.Core, msg framework.Message) error +``` + +HandleIPCEvents handles PGP-related IPC messages. +Usage: call HandleIPCEvents(...) during the package's normal workflow. + +## Functions + +### `New` + +```go +func New(c *framework.Core) (any, error) +``` + +New creates a new OpenPGP service instance. +Usage: call New(...) to create a ready-to-use value. \ No newline at end of file diff --git a/specs/crypt/pgp/README.md b/specs/crypt/pgp/README.md new file mode 100644 index 0000000..0fdd157 --- /dev/null +++ b/specs/crypt/pgp/README.md @@ -0,0 +1,77 @@ +# pgp + +**Import:** `dappco.re/go/core/crypt/crypt/pgp` + +**Files:** 1 + +## Types + +### `KeyPair` + +```go +type KeyPair struct { + PublicKey string + PrivateKey string +} +``` + +KeyPair holds armored PGP public and private keys. +Usage: use KeyPair with the other exported helpers in this package. + +## Functions + +### `CreateKeyPair` + +```go +func CreateKeyPair(name, email, password string) (*KeyPair, error) +``` + +CreateKeyPair generates a new PGP key pair for the given identity. +If password is non-empty, the private key is encrypted with it. +Returns a KeyPair with armored public and private keys. +Usage: call CreateKeyPair(...) during the package's normal workflow. + + +### `Decrypt` + +```go +func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) +``` + +Decrypt decrypts armored PGP data using the given armored private key. +If the private key is encrypted, the password is used to decrypt it first. +Usage: call Decrypt(...) during the package's normal workflow. + + +### `Encrypt` + +```go +func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) +``` + +Encrypt encrypts data for the recipient identified by their armored public key. +Returns the encrypted data as armored PGP output. +Usage: call Encrypt(...) during the package's normal workflow. + + +### `Sign` + +```go +func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) +``` + +Sign creates an armored detached signature for the given data using +the armored private key. If the key is encrypted, the password is used +to decrypt it first. +Usage: call Sign(...) during the package's normal workflow. + + +### `Verify` + +```go +func Verify(data, signature []byte, publicKeyArmor string) error +``` + +Verify verifies an armored detached signature against the given data +and armored public key. Returns nil if the signature is valid. +Usage: call Verify(...) during the package's normal workflow. \ No newline at end of file diff --git a/specs/crypt/rsa/README.md b/specs/crypt/rsa/README.md new file mode 100644 index 0000000..2ca7857 --- /dev/null +++ b/specs/crypt/rsa/README.md @@ -0,0 +1,56 @@ +# rsa + +**Import:** `dappco.re/go/core/crypt/crypt/rsa` + +**Files:** 1 + +## Types + +### `Service` + +```go +type Service struct{} +``` + +Service provides RSA functionality. +Usage: use Service with the other exported helpers in this package. + +#### Methods + +##### `Decrypt` + +```go +func (s *Service) Decrypt(privateKey, ciphertext, label []byte) ([]byte, error) +``` + +Decrypt decrypts data with a private key. +Usage: call Decrypt(...) during the package's normal workflow. + +##### `Encrypt` + +```go +func (s *Service) Encrypt(publicKey, data, label []byte) ([]byte, error) +``` + +Encrypt encrypts data with a public key. +Usage: call Encrypt(...) during the package's normal workflow. + +##### `GenerateKeyPair` + +```go +func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err error) +``` + +GenerateKeyPair creates a new RSA key pair. +Usage: call GenerateKeyPair(...) during the package's normal workflow. + +## Functions + +### `NewService` + +```go +func NewService() *Service +``` + +NewService creates and returns a new Service instance for performing RSA-related operations. +Usage: call NewService(...) to create a ready-to-use value. \ No newline at end of file diff --git a/specs/trust/README.md b/specs/trust/README.md new file mode 100644 index 0000000..1b1b7d1 --- /dev/null +++ b/specs/trust/README.md @@ -0,0 +1,599 @@ +# trust + +**Import:** `dappco.re/go/core/crypt/trust` + +**Files:** 5 + +## Types + +### `Agent` + +```go +type Agent struct { + // Name is the unique identifier for the agent (e.g., "Athena", "Clotho"). + Name string + // Tier is the agent's trust level. + Tier Tier + // ScopedRepos limits repo access for Tier 2 agents. Empty means no repo access. + // Tier 3 agents ignore this field (they have access to all repos). + ScopedRepos []string + // RateLimit is the maximum requests per minute. 0 means unlimited. + RateLimit int + // TokenExpiresAt is when the agent's token expires. + TokenExpiresAt time.Time + // CreatedAt is when the agent was registered. + CreatedAt time.Time +} +``` + +Agent represents an agent identity in the trust system. +Usage: use Agent with the other exported helpers in this package. + + +### `ApprovalQueue` + +```go +type ApprovalQueue struct { + mu sync.RWMutex + requests map[string]*ApprovalRequest + nextID int +} +``` + +ApprovalQueue manages pending approval requests for NeedsApproval decisions. +Usage: use ApprovalQueue with the other exported helpers in this package. + +#### Methods + +##### `Approve` + +```go +func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) error +``` + +Approve marks a pending request as approved. Returns an error if the +request is not found or is not in pending status. +Usage: call Approve(...) during the package's normal workflow. + +##### `Deny` + +```go +func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error +``` + +Deny marks a pending request as denied. Returns an error if the +request is not found or is not in pending status. +Usage: call Deny(...) during the package's normal workflow. + +##### `Get` + +```go +func (q *ApprovalQueue) Get(id string) *ApprovalRequest +``` + +Get returns the approval request with the given ID, or nil if not found. +Usage: call Get(...) during the package's normal workflow. + +##### `Len` + +```go +func (q *ApprovalQueue) Len() int +``` + +Len returns the total number of requests in the queue. +Usage: call Len(...) during the package's normal workflow. + +##### `Pending` + +```go +func (q *ApprovalQueue) Pending() []ApprovalRequest +``` + +Pending returns all requests with ApprovalPending status. +Usage: call Pending(...) during the package's normal workflow. + +##### `PendingSeq` + +```go +func (q *ApprovalQueue) PendingSeq() iter.Seq[ApprovalRequest] +``` + +PendingSeq returns an iterator over all requests with ApprovalPending status. +Usage: call PendingSeq(...) during the package's normal workflow. + +##### `Submit` + +```go +func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (string, error) +``` + +Submit creates a new approval request and returns its ID. +Returns an error if the agent name or capability is empty. +Usage: call Submit(...) during the package's normal workflow. + + +### `ApprovalRequest` + +```go +type ApprovalRequest struct { + // ID is the unique identifier for this request. + ID string + // Agent is the name of the requesting agent. + Agent string + // Cap is the capability being requested. + Cap Capability + // Repo is the optional repo context for repo-scoped capabilities. + Repo string + // Status is the current approval status. + Status ApprovalStatus + // Reason is a human-readable explanation from the reviewer. + Reason string + // RequestedAt is when the request was created. + RequestedAt time.Time + // ReviewedAt is when the request was reviewed (zero if pending). + ReviewedAt time.Time + // ReviewedBy is the name of the admin who reviewed the request. + ReviewedBy string +} +``` + +ApprovalRequest represents a queued capability approval request. +Usage: use ApprovalRequest with the other exported helpers in this package. + + +### `ApprovalStatus` + +```go +type ApprovalStatus int +``` + +ApprovalStatus represents the state of an approval request. +Usage: use ApprovalStatus with the other exported helpers in this package. + +#### Methods + +##### `String` + +```go +func (s ApprovalStatus) String() string +``` + +String returns the human-readable name of the approval status. +Usage: call String(...) during the package's normal workflow. + + +### `AuditEntry` + +```go +type AuditEntry struct { + // Timestamp is when the evaluation occurred. + Timestamp time.Time `json:"timestamp"` + // Agent is the name of the agent being evaluated. + Agent string `json:"agent"` + // Cap is the capability that was evaluated. + Cap Capability `json:"capability"` + // Repo is the repo context (empty if not repo-scoped). + Repo string `json:"repo,omitempty"` + // Decision is the evaluation outcome. + Decision Decision `json:"decision"` + // Reason is the human-readable reason for the decision. + Reason string `json:"reason"` +} +``` + +AuditEntry records a single policy evaluation for compliance. +Usage: use AuditEntry with the other exported helpers in this package. + + +### `AuditLog` + +```go +type AuditLog struct { + mu sync.Mutex + entries []AuditEntry + writer io.Writer +} +``` + +AuditLog is an append-only log of policy evaluations. +Usage: use AuditLog with the other exported helpers in this package. + +#### Methods + +##### `Entries` + +```go +func (l *AuditLog) Entries() []AuditEntry +``` + +Entries returns a snapshot of all audit entries. +Usage: call Entries(...) during the package's normal workflow. + +##### `EntriesFor` + +```go +func (l *AuditLog) EntriesFor(agent string) []AuditEntry +``` + +EntriesFor returns all audit entries for a specific agent. +Usage: call EntriesFor(...) during the package's normal workflow. + +##### `EntriesForSeq` + +```go +func (l *AuditLog) EntriesForSeq(agent string) iter.Seq[AuditEntry] +``` + +EntriesForSeq returns an iterator over audit entries for a specific agent. +Usage: call EntriesForSeq(...) during the package's normal workflow. + +##### `EntriesSeq` + +```go +func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] +``` + +EntriesSeq returns an iterator over all audit entries. +Usage: call EntriesSeq(...) during the package's normal workflow. + +##### `Len` + +```go +func (l *AuditLog) Len() int +``` + +Len returns the number of entries in the log. +Usage: call Len(...) during the package's normal workflow. + +##### `Record` + +```go +func (l *AuditLog) Record(result EvalResult, repo string) error +``` + +Record appends an evaluation result to the audit log. +Usage: call Record(...) during the package's normal workflow. + + +### `Capability` + +```go +type Capability string +``` + +Capability represents a specific action an agent can perform. +Usage: use Capability with the other exported helpers in this package. + + +### `Decision` + +```go +type Decision int +``` + +Decision is the result of a policy evaluation. +Usage: use Decision with the other exported helpers in this package. + +#### Methods + +##### `MarshalJSON` + +```go +func (d Decision) MarshalJSON() ([]byte, error) +``` + +MarshalJSON implements custom JSON encoding for Decision. +Usage: call MarshalJSON(...) during the package's normal workflow. + +##### `String` + +```go +func (d Decision) String() string +``` + +String returns the human-readable name of the decision. +Usage: call String(...) during the package's normal workflow. + +##### `UnmarshalJSON` + +```go +func (d *Decision) UnmarshalJSON(data []byte) error +``` + +UnmarshalJSON implements custom JSON decoding for Decision. +Usage: call UnmarshalJSON(...) during the package's normal workflow. + + +### `EvalResult` + +```go +type EvalResult struct { + Decision Decision + Agent string + Cap Capability + Reason string +} +``` + +EvalResult contains the outcome of a capability evaluation. +Usage: use EvalResult with the other exported helpers in this package. + + +### `PoliciesConfig` + +```go +type PoliciesConfig struct { + Policies []PolicyConfig `json:"policies"` +} +``` + +PoliciesConfig is the top-level configuration containing all tier policies. +Usage: use PoliciesConfig with the other exported helpers in this package. + + +### `Policy` + +```go +type Policy struct { + // Tier is the trust level this policy applies to. + Tier Tier + // Allowed lists the capabilities granted at this tier. + Allowed []Capability + // RequiresApproval lists capabilities that need human/higher-tier approval. + RequiresApproval []Capability + // Denied lists explicitly denied capabilities. + Denied []Capability +} +``` + +Policy defines the access rules for a given trust tier. +Usage: use Policy with the other exported helpers in this package. + + +### `PolicyConfig` + +```go +type PolicyConfig struct { + Tier int `json:"tier"` + Allowed []string `json:"allowed"` + RequiresApproval []string `json:"requires_approval,omitempty"` + Denied []string `json:"denied,omitempty"` +} +``` + +PolicyConfig is the JSON-serialisable representation of a trust policy. +Usage: use PolicyConfig with the other exported helpers in this package. + + +### `PolicyEngine` + +```go +type PolicyEngine struct { + registry *Registry + policies map[Tier]*Policy +} +``` + +PolicyEngine evaluates capability requests against registered policies. +Usage: use PolicyEngine with the other exported helpers in this package. + +#### Methods + +##### `ApplyPolicies` + +```go +func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error +``` + +ApplyPolicies loads policies from a reader and sets them on the engine, +replacing any existing policies for the same tiers. +Usage: call ApplyPolicies(...) during the package's normal workflow. + +##### `ApplyPoliciesFromFile` + +```go +func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error +``` + +ApplyPoliciesFromFile loads policies from a JSON file and sets them on the engine. +Usage: call ApplyPoliciesFromFile(...) during the package's normal workflow. + +##### `Evaluate` + +```go +func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) EvalResult +``` + +Evaluate checks whether the named agent can perform the given capability. +If the agent has scoped repos and the capability is repo-scoped, the repo +parameter is checked against the agent's allowed repos. +Usage: call Evaluate(...) during the package's normal workflow. + +##### `ExportPolicies` + +```go +func (pe *PolicyEngine) ExportPolicies(w io.Writer) error +``` + +ExportPolicies serialises the current policies as JSON to the given writer. +Usage: call ExportPolicies(...) during the package's normal workflow. + +##### `GetPolicy` + +```go +func (pe *PolicyEngine) GetPolicy(t Tier) *Policy +``` + +GetPolicy returns the policy for a tier, or nil if none is set. +Usage: call GetPolicy(...) during the package's normal workflow. + +##### `SetPolicy` + +```go +func (pe *PolicyEngine) SetPolicy(p Policy) error +``` + +SetPolicy replaces the policy for a given tier. +Usage: call SetPolicy(...) during the package's normal workflow. + + +### `Registry` + +```go +type Registry struct { + mu sync.RWMutex + agents map[string]*Agent +} +``` + +Registry manages agent identities and their trust tiers. +Usage: use Registry with the other exported helpers in this package. + +#### Methods + +##### `Get` + +```go +func (r *Registry) Get(name string) *Agent +``` + +Get returns the agent with the given name, or nil if not found. +Usage: call Get(...) during the package's normal workflow. + +##### `Len` + +```go +func (r *Registry) Len() int +``` + +Len returns the number of registered agents. +Usage: call Len(...) during the package's normal workflow. + +##### `List` + +```go +func (r *Registry) List() []Agent +``` + +List returns all registered agents. The returned slice is a snapshot. +Usage: call List(...) during the package's normal workflow. + +##### `ListSeq` + +```go +func (r *Registry) ListSeq() iter.Seq[Agent] +``` + +ListSeq returns an iterator over all registered agents. +Usage: call ListSeq(...) during the package's normal workflow. + +##### `Register` + +```go +func (r *Registry) Register(agent Agent) error +``` + +Register adds or updates an agent in the registry. +Returns an error if the agent name is empty or the tier is invalid. +Usage: call Register(...) during the package's normal workflow. + +##### `Remove` + +```go +func (r *Registry) Remove(name string) bool +``` + +Remove deletes an agent from the registry. +Usage: call Remove(...) during the package's normal workflow. + + +### `Tier` + +```go +type Tier int +``` + +Tier represents an agent's trust level in the system. +Usage: use Tier with the other exported helpers in this package. + +#### Methods + +##### `String` + +```go +func (t Tier) String() string +``` + +String returns the human-readable name of the tier. +Usage: call String(...) during the package's normal workflow. + +##### `Valid` + +```go +func (t Tier) Valid() bool +``` + +Valid returns true if the tier is a recognised trust level. +Usage: call Valid(...) during the package's normal workflow. + +## Functions + +### `LoadPolicies` + +```go +func LoadPolicies(r io.Reader) ([]Policy, error) +``` + +LoadPolicies reads JSON from a reader and returns parsed policies. +Usage: call LoadPolicies(...) during the package's normal workflow. + + +### `LoadPoliciesFromFile` + +```go +func LoadPoliciesFromFile(path string) ([]Policy, error) +``` + +LoadPoliciesFromFile reads a JSON file and returns parsed policies. +Usage: call LoadPoliciesFromFile(...) during the package's normal workflow. + + +### `NewApprovalQueue` + +```go +func NewApprovalQueue() *ApprovalQueue +``` + +NewApprovalQueue creates an empty approval queue. +Usage: call NewApprovalQueue(...) to create a ready-to-use value. + + +### `NewAuditLog` + +```go +func NewAuditLog(w io.Writer) *AuditLog +``` + +NewAuditLog creates an in-memory audit log. If a writer is provided, +each entry is also written as a JSON line to that writer (append-only). +Usage: call NewAuditLog(...) to create a ready-to-use value. + + +### `NewPolicyEngine` + +```go +func NewPolicyEngine(registry *Registry) *PolicyEngine +``` + +NewPolicyEngine creates a policy engine with the given registry and default policies. +Usage: call NewPolicyEngine(...) to create a ready-to-use value. + + +### `NewRegistry` + +```go +func NewRegistry() *Registry +``` + +NewRegistry creates an empty agent registry. +Usage: call NewRegistry(...) to create a ready-to-use value. \ No newline at end of file -- 2.45.3 From 82fd4e526702d9b6f225824dbf229c364bee9e60 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 27 Mar 2026 21:45:33 +0000 Subject: [PATCH 9/9] Add RFC specs for auth crypt and trust packages --- specs/auth/RFC.md | 521 +++++++++++++++++++++++++++++ specs/crypt/RFC.md | 250 ++++++++++++++ specs/crypt/chachapoly/RFC.md | 30 ++ specs/crypt/lthn/RFC.md | 63 ++++ specs/crypt/openpgp/RFC.md | 69 ++++ specs/crypt/pgp/RFC.md | 77 +++++ specs/crypt/rsa/RFC.md | 56 ++++ specs/trust/RFC.md | 599 ++++++++++++++++++++++++++++++++++ 8 files changed, 1665 insertions(+) create mode 100644 specs/auth/RFC.md create mode 100644 specs/crypt/RFC.md create mode 100644 specs/crypt/chachapoly/RFC.md create mode 100644 specs/crypt/lthn/RFC.md create mode 100644 specs/crypt/openpgp/RFC.md create mode 100644 specs/crypt/pgp/RFC.md create mode 100644 specs/crypt/rsa/RFC.md create mode 100644 specs/trust/RFC.md diff --git a/specs/auth/RFC.md b/specs/auth/RFC.md new file mode 100644 index 0000000..0ee6a29 --- /dev/null +++ b/specs/auth/RFC.md @@ -0,0 +1,521 @@ +# auth + +**Import:** `dappco.re/go/core/crypt/auth` + +**Files:** 4 + +## Types + +### `Authenticator` + +```go +type Authenticator struct { + medium io.Medium + store SessionStore + hardwareKey HardwareKey // optional hardware key (nil = software only) + challenges map[string]*Challenge // userID -> pending challenge + mu sync.RWMutex // protects challenges map only + challengeTTL time.Duration + sessionTTL time.Duration +} +``` + +Authenticator manages PGP-based challenge-response authentication. +All user data and keys are persisted through an io.Medium, which may +be backed by disk, memory (MockMedium), or any other storage backend. +Sessions are persisted via a SessionStore (in-memory by default, +optionally SQLite-backed for crash recovery). + +An optional HardwareKey can be provided via WithHardwareKey for +hardware-backed cryptographic operations (PKCS#11, YubiKey, etc.). +See auth/hardware.go for the interface definition and integration points. +Usage: create an Authenticator with New(...) and then call Register, Login, or CreateChallenge. + +#### Methods + +##### `CreateChallenge` + +```go +func (a *Authenticator) CreateChallenge(userID string) (*Challenge, error) +``` + +CreateChallenge generates a cryptographic challenge for the given user. +A random nonce is created and encrypted with the user's PGP public key. +The client must decrypt the nonce and sign it to prove key ownership. +Usage: call CreateChallenge(...) during the package's normal workflow. + +##### `DeleteUser` + +```go +func (a *Authenticator) DeleteUser(userID string) error +``` + +DeleteUser removes a user and all associated keys from storage. +The "server" user is protected and cannot be deleted (mirroring the +original TypeScript implementation's safeguard). +Usage: call DeleteUser(...) during the package's normal workflow. + +##### `IsRevoked` + +```go +func (a *Authenticator) IsRevoked(userID string) bool +``` + +IsRevoked checks whether a user's key has been revoked by inspecting the +.rev file. Returns true only if the file contains valid revocation JSON +(not the legacy "REVOCATION_PLACEHOLDER" string). +Usage: call IsRevoked(...) during the package's normal workflow. + +##### `Login` + +```go +func (a *Authenticator) Login(userID, password string) (*Session, error) +``` + +Login performs password-based authentication as a convenience method. +It verifies the password against the stored hash and, on success, +creates a new session. This bypasses the PGP challenge-response flow. + +Hash format detection: + - If a .hash file exists, its content starts with "$argon2id$" and is verified + using constant-time Argon2id comparison. + - Otherwise, falls back to legacy .lthn file with LTHN hash verification. + On successful legacy login, the password is re-hashed with Argon2id and + a .hash file is written (transparent migration). + +Usage: call Login(...) for password-based flows when challenge-response is not required. + +##### `ReadResponseFile` + +```go +func (a *Authenticator) ReadResponseFile(userID, path string) (*Session, error) +``` + +ReadResponseFile reads a signed response from a file and validates it, +completing the air-gapped authentication flow. The file must contain the +raw PGP signature bytes (armored). +Usage: call ReadResponseFile(...) during the package's normal workflow. + +##### `RefreshSession` + +```go +func (a *Authenticator) RefreshSession(token string) (*Session, error) +``` + +RefreshSession extends the expiry of an existing valid session. +Usage: call RefreshSession(...) during the package's normal workflow. + +##### `Register` + +```go +func (a *Authenticator) Register(username, password string) (*User, error) +``` + +Register creates a new user account. It hashes the username with LTHN to +produce a userID, generates a PGP keypair (protected by the given password), +and persists the public key, private key, revocation placeholder, password +hash (Argon2id), and encrypted metadata via the Medium. +Usage: call Register(...) during the package's normal workflow. + +##### `RevokeKey` + +```go +func (a *Authenticator) RevokeKey(userID, password, reason string) error +``` + +RevokeKey marks a user's key as revoked. It verifies the password first, +writes a JSON revocation record to the .rev file (replacing the placeholder), +and invalidates all sessions for the user. +Usage: call RevokeKey(...) during the package's normal workflow. + +##### `RevokeSession` + +```go +func (a *Authenticator) RevokeSession(token string) error +``` + +RevokeSession removes a session, invalidating the token immediately. +Usage: call RevokeSession(...) during the package's normal workflow. + +##### `RotateKeyPair` + +```go +func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) (*User, error) +``` + +RotateKeyPair generates a new PGP keypair for the given user, re-encrypts +their metadata with the new key, updates the password hash, and invalidates +all existing sessions. The caller must provide the current password +(oldPassword) to decrypt existing metadata and the new password (newPassword) +to protect the new keypair. +Usage: call RotateKeyPair(...) during the package's normal workflow. + +##### `StartCleanup` + +```go +func (a *Authenticator) StartCleanup(ctx context.Context, interval time.Duration) +``` + +StartCleanup runs a background goroutine that periodically removes expired +sessions from the store. It stops when the context is cancelled. +Usage: call StartCleanup(...) during the package's normal workflow. + +##### `ValidateResponse` + +```go +func (a *Authenticator) ValidateResponse(userID string, signedNonce []byte) (*Session, error) +``` + +ValidateResponse verifies a signed nonce from the client. The client must +have decrypted the challenge nonce and signed it with their private key. +On success, a new session is created and returned. +Usage: call ValidateResponse(...) during the package's normal workflow. + +##### `ValidateSession` + +```go +func (a *Authenticator) ValidateSession(token string) (*Session, error) +``` + +ValidateSession checks whether a token maps to a valid, non-expired session. +Usage: call ValidateSession(...) during the package's normal workflow. + +##### `WriteChallengeFile` + +```go +func (a *Authenticator) WriteChallengeFile(userID, path string) error +``` + +WriteChallengeFile writes an encrypted challenge to a file for air-gapped +(courier) transport. The challenge is created and then its encrypted nonce +is written to the specified path on the Medium. +Usage: call WriteChallengeFile(...) during the package's normal workflow. + + +### `Challenge` + +```go +type Challenge struct { + Nonce []byte `json:"nonce"` + Encrypted string `json:"encrypted"` // PGP-encrypted nonce (armored) + ExpiresAt time.Time `json:"expires_at"` +} +``` + +Challenge is a PGP-encrypted nonce sent to a client during authentication. +Usage: use Challenge with the other exported helpers in this package. + + +### `HardwareKey` + +```go +type HardwareKey interface { + // Sign produces a cryptographic signature over the given data using the + // hardware-stored private key. The signature format depends on the + // underlying device (e.g. ECDSA, RSA-PSS, EdDSA). + Sign(data []byte) ([]byte, error) + + // Decrypt decrypts ciphertext using the hardware-stored private key. + // The ciphertext format must match what the device expects (e.g. RSA-OAEP). + Decrypt(ciphertext []byte) ([]byte, error) + + // GetPublicKey returns the PEM or armored public key corresponding to the + // hardware-stored private key. + GetPublicKey() (string, error) + + // IsAvailable reports whether the hardware key device is currently + // connected and operational. Callers should check this before attempting + // Sign or Decrypt to provide graceful fallback behaviour. + IsAvailable() bool +} +``` + +HardwareKey defines the contract for hardware-backed cryptographic operations. +Implementations should wrap PKCS#11 tokens, YubiKeys, TPM modules, or +similar tamper-resistant devices. + +All methods must be safe for concurrent use. +Usage: implement HardwareKey and pass it to WithHardwareKey(...) to wire hardware-backed auth into New(...). + + +### `MemorySessionStore` + +```go +type MemorySessionStore struct { + mu sync.RWMutex + sessions map[string]*Session +} +``` + +MemorySessionStore is an in-memory SessionStore backed by a map. +Usage: use MemorySessionStore with the other exported helpers in this package. + +#### Methods + +##### `Cleanup` + +```go +func (m *MemorySessionStore) Cleanup() (int, error) +``` + +Cleanup removes all expired sessions and returns the count removed. +Usage: call Cleanup(...) during the package's normal workflow. + +##### `Delete` + +```go +func (m *MemorySessionStore) Delete(token string) error +``` + +Delete removes a session by token. +Usage: call Delete(...) during the package's normal workflow. + +##### `DeleteByUser` + +```go +func (m *MemorySessionStore) DeleteByUser(userID string) error +``` + +DeleteByUser removes all sessions belonging to the given user. +Usage: call DeleteByUser(...) during the package's normal workflow. + +##### `Get` + +```go +func (m *MemorySessionStore) Get(token string) (*Session, error) +``` + +Get retrieves a session by token. +Usage: call Get(...) during the package's normal workflow. + +##### `Set` + +```go +func (m *MemorySessionStore) Set(session *Session) error +``` + +Set stores a session, keyed by its token. +Usage: call Set(...) during the package's normal workflow. + + +### `Option` + +```go +type Option func(*Authenticator) +``` + +Option configures an Authenticator. +Usage: use Option with the other exported helpers in this package. + + +### `Revocation` + +```go +type Revocation struct { + UserID string `json:"user_id"` + Reason string `json:"reason"` + RevokedAt time.Time `json:"revoked_at"` +} +``` + +Revocation records the details of a revoked user key. +Stored as JSON in the user's .rev file, replacing the legacy placeholder. +Usage: use Revocation with the other exported helpers in this package. + + +### `SQLiteSessionStore` + +```go +type SQLiteSessionStore struct { + mu sync.Mutex + store *store.Store +} +``` + +SQLiteSessionStore is a SessionStore backed by core/store (SQLite KV). +A mutex serialises all operations because SQLite is single-writer. +Usage: use SQLiteSessionStore with the other exported helpers in this package. + +#### Methods + +##### `Cleanup` + +```go +func (s *SQLiteSessionStore) Cleanup() (int, error) +``` + +Cleanup removes all expired sessions and returns the count removed. +Usage: call Cleanup(...) during the package's normal workflow. + +##### `Close` + +```go +func (s *SQLiteSessionStore) Close() error +``` + +Close closes the underlying SQLite store. +Usage: call Close(...) during the package's normal workflow. + +##### `Delete` + +```go +func (s *SQLiteSessionStore) Delete(token string) error +``` + +Delete removes a session by token from SQLite. +Usage: call Delete(...) during the package's normal workflow. + +##### `DeleteByUser` + +```go +func (s *SQLiteSessionStore) DeleteByUser(userID string) error +``` + +DeleteByUser removes all sessions belonging to the given user. +Usage: call DeleteByUser(...) during the package's normal workflow. + +##### `Get` + +```go +func (s *SQLiteSessionStore) Get(token string) (*Session, error) +``` + +Get retrieves a session by token from SQLite. +Usage: call Get(...) during the package's normal workflow. + +##### `Set` + +```go +func (s *SQLiteSessionStore) Set(session *Session) error +``` + +Set stores a session in SQLite, keyed by its token. +Usage: call Set(...) during the package's normal workflow. + + +### `Session` + +```go +type Session struct { + Token string `json:"token"` + UserID string `json:"user_id"` + ExpiresAt time.Time `json:"expires_at"` +} +``` + +Session represents an authenticated session. +Usage: use Session with the other exported helpers in this package. + + +### `SessionStore` + +```go +type SessionStore interface { + Get(token string) (*Session, error) + Set(session *Session) error + Delete(token string) error + DeleteByUser(userID string) error + Cleanup() (int, error) // Remove expired sessions, return count removed +} +``` + +SessionStore abstracts session persistence. +Usage: use SessionStore with the other exported helpers in this package. + + +### `User` + +```go +type User struct { + PublicKey string `json:"public_key"` + KeyID string `json:"key_id"` + Fingerprint string `json:"fingerprint"` + PasswordHash string `json:"password_hash"` // Argon2id (new) or LTHN (legacy) + Created time.Time `json:"created"` + LastLogin time.Time `json:"last_login"` +} +``` + +User represents a registered user with PGP credentials. +Usage: use User with the other exported helpers in this package. + +## Functions + +### `New` + +```go +func New(m io.Medium, opts ...Option) *Authenticator +``` + +New creates an Authenticator that persists user data via the given Medium. +By default, sessions are stored in memory. Use WithSessionStore to provide +a persistent implementation (e.g. SQLiteSessionStore). +Usage: call New(...) to create a ready-to-use value. + + +### `NewMemorySessionStore` + +```go +func NewMemorySessionStore() *MemorySessionStore +``` + +NewMemorySessionStore creates a new in-memory session store. +Usage: call NewMemorySessionStore(...) to create a ready-to-use value. + + +### `NewSQLiteSessionStore` + +```go +func NewSQLiteSessionStore(dbPath string) (*SQLiteSessionStore, error) +``` + +NewSQLiteSessionStore creates a new SQLite-backed session store. +Use ":memory:" for testing or a file path for persistent storage. +Usage: call NewSQLiteSessionStore(...) to create a ready-to-use value. + + +### `WithChallengeTTL` + +```go +func WithChallengeTTL(d time.Duration) Option +``` + +WithChallengeTTL sets the lifetime of a challenge before it expires. +Usage: pass WithChallengeTTL(...) into the related constructor to adjust the default behaviour. + + +### `WithHardwareKey` + +```go +func WithHardwareKey(hk HardwareKey) Option +``` + +WithHardwareKey configures the Authenticator to use a hardware key for +cryptographic operations where supported. When set, the Authenticator may +delegate signing, decryption, and public key retrieval to the hardware +device instead of using software PGP keys. + +This is a forward-looking option — integration points are documented in +auth.go but not yet wired up. +Usage: pass WithHardwareKey(...) into New(...) to enable a HardwareKey implementation. + + +### `WithSessionStore` + +```go +func WithSessionStore(s SessionStore) Option +``` + +WithSessionStore sets the SessionStore implementation. +If not provided, an in-memory store is used (sessions lost on restart). +Usage: pass WithSessionStore(...) into the related constructor to adjust the default behaviour. + + +### `WithSessionTTL` + +```go +func WithSessionTTL(d time.Duration) Option +``` + +WithSessionTTL sets the lifetime of a session before it expires. +Usage: pass WithSessionTTL(...) into the related constructor to adjust the default behaviour. \ No newline at end of file diff --git a/specs/crypt/RFC.md b/specs/crypt/RFC.md new file mode 100644 index 0000000..254737c --- /dev/null +++ b/specs/crypt/RFC.md @@ -0,0 +1,250 @@ +# crypt + +**Import:** `dappco.re/go/core/crypt/crypt` + +**Files:** 6 + +## Types + +None. + +## Functions + +### `AESGCMDecrypt` + +```go +func AESGCMDecrypt(ciphertext, key []byte) ([]byte, error) +``` + +AESGCMDecrypt decrypts ciphertext encrypted with AESGCMEncrypt. +The key must be 32 bytes. Expects the nonce prepended to the ciphertext. +Usage: call AESGCMDecrypt(...) during the package's normal workflow. + + +### `AESGCMEncrypt` + +```go +func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) +``` + +AESGCMEncrypt encrypts plaintext using AES-256-GCM. +The key must be 32 bytes. The nonce is randomly generated and prepended +to the ciphertext. +Usage: call AESGCMEncrypt(...) during the package's normal workflow. + + +### `ChaCha20Decrypt` + +```go +func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) +``` + +ChaCha20Decrypt decrypts ciphertext encrypted with ChaCha20Encrypt. +The key must be 32 bytes. Expects the nonce prepended to the ciphertext. +Usage: call ChaCha20Decrypt(...) during the package's normal workflow. + + +### `ChaCha20Encrypt` + +```go +func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) +``` + +ChaCha20Encrypt encrypts plaintext using ChaCha20-Poly1305. +The key must be 32 bytes. The nonce is randomly generated and prepended +to the ciphertext. +Usage: call ChaCha20Encrypt(...) during the package's normal workflow. + + +### `Decrypt` + +```go +func Decrypt(ciphertext, passphrase []byte) ([]byte, error) +``` + +Decrypt decrypts data encrypted with Encrypt. +Expects format: salt (16 bytes) + nonce (24 bytes) + ciphertext. +Usage: call Decrypt(...) during the package's normal workflow. + + +### `DecryptAES` + +```go +func DecryptAES(ciphertext, passphrase []byte) ([]byte, error) +``` + +DecryptAES decrypts data encrypted with EncryptAES. +Expects format: salt (16 bytes) + nonce (12 bytes) + ciphertext. +Usage: call DecryptAES(...) during the package's normal workflow. + + +### `DeriveKey` + +```go +func DeriveKey(passphrase, salt []byte, keyLen uint32) []byte +``` + +DeriveKey derives a key from a passphrase using Argon2id with default parameters. +The salt must be argon2SaltLen bytes. keyLen specifies the desired key length. +Usage: call DeriveKey(...) during the package's normal workflow. + + +### `DeriveKeyScrypt` + +```go +func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error) +``` + +DeriveKeyScrypt derives a key from a passphrase using scrypt. +Uses recommended parameters: N=32768, r=8, p=1. +Usage: call DeriveKeyScrypt(...) during the package's normal workflow. + + +### `Encrypt` + +```go +func Encrypt(plaintext, passphrase []byte) ([]byte, error) +``` + +Encrypt encrypts data with a passphrase using ChaCha20-Poly1305. +A random salt is generated and prepended to the output. +Format: salt (16 bytes) + nonce (24 bytes) + ciphertext. +Usage: call Encrypt(...) during the package's normal workflow. + + +### `EncryptAES` + +```go +func EncryptAES(plaintext, passphrase []byte) ([]byte, error) +``` + +EncryptAES encrypts data using AES-256-GCM with a passphrase. +A random salt is generated and prepended to the output. +Format: salt (16 bytes) + nonce (12 bytes) + ciphertext. +Usage: call EncryptAES(...) during the package's normal workflow. + + +### `HKDF` + +```go +func HKDF(secret, salt, info []byte, keyLen int) ([]byte, error) +``` + +HKDF derives a key using HKDF-SHA256. +secret is the input keying material, salt is optional (can be nil), +info is optional context, and keyLen is the desired output length. +Usage: call HKDF(...) during the package's normal workflow. + + +### `HMACSHA256` + +```go +func HMACSHA256(message, key []byte) []byte +``` + +HMACSHA256 computes the HMAC-SHA256 of a message using the given key. +Usage: call HMACSHA256(...) during the package's normal workflow. + + +### `HMACSHA512` + +```go +func HMACSHA512(message, key []byte) []byte +``` + +HMACSHA512 computes the HMAC-SHA512 of a message using the given key. +Usage: call HMACSHA512(...) during the package's normal workflow. + + +### `HashBcrypt` + +```go +func HashBcrypt(password string, cost int) (string, error) +``` + +HashBcrypt hashes a password using bcrypt with the given cost. +Cost must be between bcrypt.MinCost and bcrypt.MaxCost. +Usage: call HashBcrypt(...) during the package's normal workflow. + + +### `HashPassword` + +```go +func HashPassword(password string) (string, error) +``` + +HashPassword hashes a password using Argon2id with default parameters. +Returns a string in the format: $argon2id$v=19$m=65536,t=3,p=4$$ +Usage: call HashPassword(...) during the package's normal workflow. + + +### `SHA256File` + +```go +func SHA256File(path string) (string, error) +``` + +SHA256File computes the SHA-256 checksum of a file and returns it as a hex string. +Usage: call SHA256File(...) during the package's normal workflow. + + +### `SHA256Sum` + +```go +func SHA256Sum(data []byte) string +``` + +SHA256Sum computes the SHA-256 checksum of data and returns it as a hex string. +Usage: call SHA256Sum(...) during the package's normal workflow. + + +### `SHA512File` + +```go +func SHA512File(path string) (string, error) +``` + +SHA512File computes the SHA-512 checksum of a file and returns it as a hex string. +Usage: call SHA512File(...) during the package's normal workflow. + + +### `SHA512Sum` + +```go +func SHA512Sum(data []byte) string +``` + +SHA512Sum computes the SHA-512 checksum of data and returns it as a hex string. +Usage: call SHA512Sum(...) during the package's normal workflow. + + +### `VerifyBcrypt` + +```go +func VerifyBcrypt(password, hash string) (bool, error) +``` + +VerifyBcrypt verifies a password against a bcrypt hash. +Usage: call VerifyBcrypt(...) during the package's normal workflow. + + +### `VerifyHMAC` + +```go +func VerifyHMAC(message, key, mac []byte, hashFunc func() hash.Hash) bool +``` + +VerifyHMAC verifies an HMAC using constant-time comparison. +hashFunc should be sha256.New, sha512.New, etc. +Usage: call VerifyHMAC(...) during the package's normal workflow. + + +### `VerifyPassword` + +```go +func VerifyPassword(password, hash string) (bool, error) +``` + +VerifyPassword verifies a password against an Argon2id hash string. +The hash must be in the format produced by HashPassword. +Usage: call VerifyPassword(...) during the package's normal workflow. \ No newline at end of file diff --git a/specs/crypt/chachapoly/RFC.md b/specs/crypt/chachapoly/RFC.md new file mode 100644 index 0000000..8f081c9 --- /dev/null +++ b/specs/crypt/chachapoly/RFC.md @@ -0,0 +1,30 @@ +# chachapoly + +**Import:** `dappco.re/go/core/crypt/crypt/chachapoly` + +**Files:** 1 + +## Types + +None. + +## Functions + +### `Decrypt` + +```go +func Decrypt(ciphertext []byte, key []byte) ([]byte, error) +``` + +Decrypt decrypts data using ChaCha20-Poly1305. +Usage: call Decrypt(...) during the package's normal workflow. + + +### `Encrypt` + +```go +func Encrypt(plaintext []byte, key []byte) ([]byte, error) +``` + +Encrypt encrypts data using ChaCha20-Poly1305. +Usage: call Encrypt(...) during the package's normal workflow. \ No newline at end of file diff --git a/specs/crypt/lthn/RFC.md b/specs/crypt/lthn/RFC.md new file mode 100644 index 0000000..63516f0 --- /dev/null +++ b/specs/crypt/lthn/RFC.md @@ -0,0 +1,63 @@ +# lthn + +**Import:** `dappco.re/go/core/crypt/crypt/lthn` + +**Files:** 1 + +## Types + +None. + +## Functions + +### `GetKeyMap` + +```go +func GetKeyMap() map[rune]rune +``` + +GetKeyMap returns the current character substitution map. +Usage: call GetKeyMap(...) during the package's normal workflow. + + +### `Hash` + +```go +func Hash(input string) string +``` + +Hash computes the LTHN hash of the input string. + +The algorithm: + 1. Derive a quasi-salt by reversing the input and applying character substitutions + 2. Concatenate: input + salt + 3. Compute SHA-256 of the concatenated string + 4. Return the hex-encoded digest (64 characters, lowercase) + +The same input always produces the same hash, enabling verification +without storing a separate salt value. +Usage: call Hash(...) when you need a deterministic content-style digest rather than a password hash. + + +### `SetKeyMap` + +```go +func SetKeyMap(newKeyMap map[rune]rune) +``` + +SetKeyMap replaces the default character substitution map. +Use this to customize the quasi-salt derivation for specific applications. +Changes affect all subsequent Hash and Verify calls. +Usage: call SetKeyMap(...) during the package's normal workflow. + + +### `Verify` + +```go +func Verify(input string, hash string) bool +``` + +Verify checks if an input string produces the given hash. +Returns true if Hash(input) equals the provided hash value. +Uses constant-time comparison to prevent timing attacks. +Usage: call Verify(...) during the package's normal workflow. \ No newline at end of file diff --git a/specs/crypt/openpgp/RFC.md b/specs/crypt/openpgp/RFC.md new file mode 100644 index 0000000..a327f34 --- /dev/null +++ b/specs/crypt/openpgp/RFC.md @@ -0,0 +1,69 @@ +# openpgp + +**Import:** `dappco.re/go/core/crypt/crypt/openpgp` + +**Files:** 1 + +## Types + +### `Service` + +```go +type Service struct { + core *framework.Core +} +``` + +Service provides OpenPGP cryptographic operations. +Usage: use Service with the other exported helpers in this package. + +#### Methods + +##### `CreateKeyPair` + +```go +func (s *Service) CreateKeyPair(name, passphrase string) (string, error) +``` + +CreateKeyPair generates a new RSA-4096 PGP keypair. +Returns the armored private key string. +Usage: call CreateKeyPair(...) during the package's normal workflow. + +##### `DecryptPGP` + +```go +func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any) (string, error) +``` + +DecryptPGP decrypts a PGP message using the provided armored private key and passphrase. +Usage: call DecryptPGP(...) during the package's normal workflow. + +##### `EncryptPGP` + +```go +func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opts ...any) (string, error) +``` + +EncryptPGP encrypts data for a recipient identified by their public key (armored string in recipientPath). +The encrypted data is written to the provided writer and also returned as an armored string. +Usage: call EncryptPGP(...) during the package's normal workflow. + +##### `HandleIPCEvents` + +```go +func (s *Service) HandleIPCEvents(c *framework.Core, msg framework.Message) error +``` + +HandleIPCEvents handles PGP-related IPC messages. +Usage: call HandleIPCEvents(...) during the package's normal workflow. + +## Functions + +### `New` + +```go +func New(c *framework.Core) (any, error) +``` + +New creates a new OpenPGP service instance. +Usage: call New(...) to create a ready-to-use value. \ No newline at end of file diff --git a/specs/crypt/pgp/RFC.md b/specs/crypt/pgp/RFC.md new file mode 100644 index 0000000..0fdd157 --- /dev/null +++ b/specs/crypt/pgp/RFC.md @@ -0,0 +1,77 @@ +# pgp + +**Import:** `dappco.re/go/core/crypt/crypt/pgp` + +**Files:** 1 + +## Types + +### `KeyPair` + +```go +type KeyPair struct { + PublicKey string + PrivateKey string +} +``` + +KeyPair holds armored PGP public and private keys. +Usage: use KeyPair with the other exported helpers in this package. + +## Functions + +### `CreateKeyPair` + +```go +func CreateKeyPair(name, email, password string) (*KeyPair, error) +``` + +CreateKeyPair generates a new PGP key pair for the given identity. +If password is non-empty, the private key is encrypted with it. +Returns a KeyPair with armored public and private keys. +Usage: call CreateKeyPair(...) during the package's normal workflow. + + +### `Decrypt` + +```go +func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) +``` + +Decrypt decrypts armored PGP data using the given armored private key. +If the private key is encrypted, the password is used to decrypt it first. +Usage: call Decrypt(...) during the package's normal workflow. + + +### `Encrypt` + +```go +func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) +``` + +Encrypt encrypts data for the recipient identified by their armored public key. +Returns the encrypted data as armored PGP output. +Usage: call Encrypt(...) during the package's normal workflow. + + +### `Sign` + +```go +func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) +``` + +Sign creates an armored detached signature for the given data using +the armored private key. If the key is encrypted, the password is used +to decrypt it first. +Usage: call Sign(...) during the package's normal workflow. + + +### `Verify` + +```go +func Verify(data, signature []byte, publicKeyArmor string) error +``` + +Verify verifies an armored detached signature against the given data +and armored public key. Returns nil if the signature is valid. +Usage: call Verify(...) during the package's normal workflow. \ No newline at end of file diff --git a/specs/crypt/rsa/RFC.md b/specs/crypt/rsa/RFC.md new file mode 100644 index 0000000..2ca7857 --- /dev/null +++ b/specs/crypt/rsa/RFC.md @@ -0,0 +1,56 @@ +# rsa + +**Import:** `dappco.re/go/core/crypt/crypt/rsa` + +**Files:** 1 + +## Types + +### `Service` + +```go +type Service struct{} +``` + +Service provides RSA functionality. +Usage: use Service with the other exported helpers in this package. + +#### Methods + +##### `Decrypt` + +```go +func (s *Service) Decrypt(privateKey, ciphertext, label []byte) ([]byte, error) +``` + +Decrypt decrypts data with a private key. +Usage: call Decrypt(...) during the package's normal workflow. + +##### `Encrypt` + +```go +func (s *Service) Encrypt(publicKey, data, label []byte) ([]byte, error) +``` + +Encrypt encrypts data with a public key. +Usage: call Encrypt(...) during the package's normal workflow. + +##### `GenerateKeyPair` + +```go +func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err error) +``` + +GenerateKeyPair creates a new RSA key pair. +Usage: call GenerateKeyPair(...) during the package's normal workflow. + +## Functions + +### `NewService` + +```go +func NewService() *Service +``` + +NewService creates and returns a new Service instance for performing RSA-related operations. +Usage: call NewService(...) to create a ready-to-use value. \ No newline at end of file diff --git a/specs/trust/RFC.md b/specs/trust/RFC.md new file mode 100644 index 0000000..1b1b7d1 --- /dev/null +++ b/specs/trust/RFC.md @@ -0,0 +1,599 @@ +# trust + +**Import:** `dappco.re/go/core/crypt/trust` + +**Files:** 5 + +## Types + +### `Agent` + +```go +type Agent struct { + // Name is the unique identifier for the agent (e.g., "Athena", "Clotho"). + Name string + // Tier is the agent's trust level. + Tier Tier + // ScopedRepos limits repo access for Tier 2 agents. Empty means no repo access. + // Tier 3 agents ignore this field (they have access to all repos). + ScopedRepos []string + // RateLimit is the maximum requests per minute. 0 means unlimited. + RateLimit int + // TokenExpiresAt is when the agent's token expires. + TokenExpiresAt time.Time + // CreatedAt is when the agent was registered. + CreatedAt time.Time +} +``` + +Agent represents an agent identity in the trust system. +Usage: use Agent with the other exported helpers in this package. + + +### `ApprovalQueue` + +```go +type ApprovalQueue struct { + mu sync.RWMutex + requests map[string]*ApprovalRequest + nextID int +} +``` + +ApprovalQueue manages pending approval requests for NeedsApproval decisions. +Usage: use ApprovalQueue with the other exported helpers in this package. + +#### Methods + +##### `Approve` + +```go +func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) error +``` + +Approve marks a pending request as approved. Returns an error if the +request is not found or is not in pending status. +Usage: call Approve(...) during the package's normal workflow. + +##### `Deny` + +```go +func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error +``` + +Deny marks a pending request as denied. Returns an error if the +request is not found or is not in pending status. +Usage: call Deny(...) during the package's normal workflow. + +##### `Get` + +```go +func (q *ApprovalQueue) Get(id string) *ApprovalRequest +``` + +Get returns the approval request with the given ID, or nil if not found. +Usage: call Get(...) during the package's normal workflow. + +##### `Len` + +```go +func (q *ApprovalQueue) Len() int +``` + +Len returns the total number of requests in the queue. +Usage: call Len(...) during the package's normal workflow. + +##### `Pending` + +```go +func (q *ApprovalQueue) Pending() []ApprovalRequest +``` + +Pending returns all requests with ApprovalPending status. +Usage: call Pending(...) during the package's normal workflow. + +##### `PendingSeq` + +```go +func (q *ApprovalQueue) PendingSeq() iter.Seq[ApprovalRequest] +``` + +PendingSeq returns an iterator over all requests with ApprovalPending status. +Usage: call PendingSeq(...) during the package's normal workflow. + +##### `Submit` + +```go +func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (string, error) +``` + +Submit creates a new approval request and returns its ID. +Returns an error if the agent name or capability is empty. +Usage: call Submit(...) during the package's normal workflow. + + +### `ApprovalRequest` + +```go +type ApprovalRequest struct { + // ID is the unique identifier for this request. + ID string + // Agent is the name of the requesting agent. + Agent string + // Cap is the capability being requested. + Cap Capability + // Repo is the optional repo context for repo-scoped capabilities. + Repo string + // Status is the current approval status. + Status ApprovalStatus + // Reason is a human-readable explanation from the reviewer. + Reason string + // RequestedAt is when the request was created. + RequestedAt time.Time + // ReviewedAt is when the request was reviewed (zero if pending). + ReviewedAt time.Time + // ReviewedBy is the name of the admin who reviewed the request. + ReviewedBy string +} +``` + +ApprovalRequest represents a queued capability approval request. +Usage: use ApprovalRequest with the other exported helpers in this package. + + +### `ApprovalStatus` + +```go +type ApprovalStatus int +``` + +ApprovalStatus represents the state of an approval request. +Usage: use ApprovalStatus with the other exported helpers in this package. + +#### Methods + +##### `String` + +```go +func (s ApprovalStatus) String() string +``` + +String returns the human-readable name of the approval status. +Usage: call String(...) during the package's normal workflow. + + +### `AuditEntry` + +```go +type AuditEntry struct { + // Timestamp is when the evaluation occurred. + Timestamp time.Time `json:"timestamp"` + // Agent is the name of the agent being evaluated. + Agent string `json:"agent"` + // Cap is the capability that was evaluated. + Cap Capability `json:"capability"` + // Repo is the repo context (empty if not repo-scoped). + Repo string `json:"repo,omitempty"` + // Decision is the evaluation outcome. + Decision Decision `json:"decision"` + // Reason is the human-readable reason for the decision. + Reason string `json:"reason"` +} +``` + +AuditEntry records a single policy evaluation for compliance. +Usage: use AuditEntry with the other exported helpers in this package. + + +### `AuditLog` + +```go +type AuditLog struct { + mu sync.Mutex + entries []AuditEntry + writer io.Writer +} +``` + +AuditLog is an append-only log of policy evaluations. +Usage: use AuditLog with the other exported helpers in this package. + +#### Methods + +##### `Entries` + +```go +func (l *AuditLog) Entries() []AuditEntry +``` + +Entries returns a snapshot of all audit entries. +Usage: call Entries(...) during the package's normal workflow. + +##### `EntriesFor` + +```go +func (l *AuditLog) EntriesFor(agent string) []AuditEntry +``` + +EntriesFor returns all audit entries for a specific agent. +Usage: call EntriesFor(...) during the package's normal workflow. + +##### `EntriesForSeq` + +```go +func (l *AuditLog) EntriesForSeq(agent string) iter.Seq[AuditEntry] +``` + +EntriesForSeq returns an iterator over audit entries for a specific agent. +Usage: call EntriesForSeq(...) during the package's normal workflow. + +##### `EntriesSeq` + +```go +func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] +``` + +EntriesSeq returns an iterator over all audit entries. +Usage: call EntriesSeq(...) during the package's normal workflow. + +##### `Len` + +```go +func (l *AuditLog) Len() int +``` + +Len returns the number of entries in the log. +Usage: call Len(...) during the package's normal workflow. + +##### `Record` + +```go +func (l *AuditLog) Record(result EvalResult, repo string) error +``` + +Record appends an evaluation result to the audit log. +Usage: call Record(...) during the package's normal workflow. + + +### `Capability` + +```go +type Capability string +``` + +Capability represents a specific action an agent can perform. +Usage: use Capability with the other exported helpers in this package. + + +### `Decision` + +```go +type Decision int +``` + +Decision is the result of a policy evaluation. +Usage: use Decision with the other exported helpers in this package. + +#### Methods + +##### `MarshalJSON` + +```go +func (d Decision) MarshalJSON() ([]byte, error) +``` + +MarshalJSON implements custom JSON encoding for Decision. +Usage: call MarshalJSON(...) during the package's normal workflow. + +##### `String` + +```go +func (d Decision) String() string +``` + +String returns the human-readable name of the decision. +Usage: call String(...) during the package's normal workflow. + +##### `UnmarshalJSON` + +```go +func (d *Decision) UnmarshalJSON(data []byte) error +``` + +UnmarshalJSON implements custom JSON decoding for Decision. +Usage: call UnmarshalJSON(...) during the package's normal workflow. + + +### `EvalResult` + +```go +type EvalResult struct { + Decision Decision + Agent string + Cap Capability + Reason string +} +``` + +EvalResult contains the outcome of a capability evaluation. +Usage: use EvalResult with the other exported helpers in this package. + + +### `PoliciesConfig` + +```go +type PoliciesConfig struct { + Policies []PolicyConfig `json:"policies"` +} +``` + +PoliciesConfig is the top-level configuration containing all tier policies. +Usage: use PoliciesConfig with the other exported helpers in this package. + + +### `Policy` + +```go +type Policy struct { + // Tier is the trust level this policy applies to. + Tier Tier + // Allowed lists the capabilities granted at this tier. + Allowed []Capability + // RequiresApproval lists capabilities that need human/higher-tier approval. + RequiresApproval []Capability + // Denied lists explicitly denied capabilities. + Denied []Capability +} +``` + +Policy defines the access rules for a given trust tier. +Usage: use Policy with the other exported helpers in this package. + + +### `PolicyConfig` + +```go +type PolicyConfig struct { + Tier int `json:"tier"` + Allowed []string `json:"allowed"` + RequiresApproval []string `json:"requires_approval,omitempty"` + Denied []string `json:"denied,omitempty"` +} +``` + +PolicyConfig is the JSON-serialisable representation of a trust policy. +Usage: use PolicyConfig with the other exported helpers in this package. + + +### `PolicyEngine` + +```go +type PolicyEngine struct { + registry *Registry + policies map[Tier]*Policy +} +``` + +PolicyEngine evaluates capability requests against registered policies. +Usage: use PolicyEngine with the other exported helpers in this package. + +#### Methods + +##### `ApplyPolicies` + +```go +func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error +``` + +ApplyPolicies loads policies from a reader and sets them on the engine, +replacing any existing policies for the same tiers. +Usage: call ApplyPolicies(...) during the package's normal workflow. + +##### `ApplyPoliciesFromFile` + +```go +func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error +``` + +ApplyPoliciesFromFile loads policies from a JSON file and sets them on the engine. +Usage: call ApplyPoliciesFromFile(...) during the package's normal workflow. + +##### `Evaluate` + +```go +func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) EvalResult +``` + +Evaluate checks whether the named agent can perform the given capability. +If the agent has scoped repos and the capability is repo-scoped, the repo +parameter is checked against the agent's allowed repos. +Usage: call Evaluate(...) during the package's normal workflow. + +##### `ExportPolicies` + +```go +func (pe *PolicyEngine) ExportPolicies(w io.Writer) error +``` + +ExportPolicies serialises the current policies as JSON to the given writer. +Usage: call ExportPolicies(...) during the package's normal workflow. + +##### `GetPolicy` + +```go +func (pe *PolicyEngine) GetPolicy(t Tier) *Policy +``` + +GetPolicy returns the policy for a tier, or nil if none is set. +Usage: call GetPolicy(...) during the package's normal workflow. + +##### `SetPolicy` + +```go +func (pe *PolicyEngine) SetPolicy(p Policy) error +``` + +SetPolicy replaces the policy for a given tier. +Usage: call SetPolicy(...) during the package's normal workflow. + + +### `Registry` + +```go +type Registry struct { + mu sync.RWMutex + agents map[string]*Agent +} +``` + +Registry manages agent identities and their trust tiers. +Usage: use Registry with the other exported helpers in this package. + +#### Methods + +##### `Get` + +```go +func (r *Registry) Get(name string) *Agent +``` + +Get returns the agent with the given name, or nil if not found. +Usage: call Get(...) during the package's normal workflow. + +##### `Len` + +```go +func (r *Registry) Len() int +``` + +Len returns the number of registered agents. +Usage: call Len(...) during the package's normal workflow. + +##### `List` + +```go +func (r *Registry) List() []Agent +``` + +List returns all registered agents. The returned slice is a snapshot. +Usage: call List(...) during the package's normal workflow. + +##### `ListSeq` + +```go +func (r *Registry) ListSeq() iter.Seq[Agent] +``` + +ListSeq returns an iterator over all registered agents. +Usage: call ListSeq(...) during the package's normal workflow. + +##### `Register` + +```go +func (r *Registry) Register(agent Agent) error +``` + +Register adds or updates an agent in the registry. +Returns an error if the agent name is empty or the tier is invalid. +Usage: call Register(...) during the package's normal workflow. + +##### `Remove` + +```go +func (r *Registry) Remove(name string) bool +``` + +Remove deletes an agent from the registry. +Usage: call Remove(...) during the package's normal workflow. + + +### `Tier` + +```go +type Tier int +``` + +Tier represents an agent's trust level in the system. +Usage: use Tier with the other exported helpers in this package. + +#### Methods + +##### `String` + +```go +func (t Tier) String() string +``` + +String returns the human-readable name of the tier. +Usage: call String(...) during the package's normal workflow. + +##### `Valid` + +```go +func (t Tier) Valid() bool +``` + +Valid returns true if the tier is a recognised trust level. +Usage: call Valid(...) during the package's normal workflow. + +## Functions + +### `LoadPolicies` + +```go +func LoadPolicies(r io.Reader) ([]Policy, error) +``` + +LoadPolicies reads JSON from a reader and returns parsed policies. +Usage: call LoadPolicies(...) during the package's normal workflow. + + +### `LoadPoliciesFromFile` + +```go +func LoadPoliciesFromFile(path string) ([]Policy, error) +``` + +LoadPoliciesFromFile reads a JSON file and returns parsed policies. +Usage: call LoadPoliciesFromFile(...) during the package's normal workflow. + + +### `NewApprovalQueue` + +```go +func NewApprovalQueue() *ApprovalQueue +``` + +NewApprovalQueue creates an empty approval queue. +Usage: call NewApprovalQueue(...) to create a ready-to-use value. + + +### `NewAuditLog` + +```go +func NewAuditLog(w io.Writer) *AuditLog +``` + +NewAuditLog creates an in-memory audit log. If a writer is provided, +each entry is also written as a JSON line to that writer (append-only). +Usage: call NewAuditLog(...) to create a ready-to-use value. + + +### `NewPolicyEngine` + +```go +func NewPolicyEngine(registry *Registry) *PolicyEngine +``` + +NewPolicyEngine creates a policy engine with the given registry and default policies. +Usage: call NewPolicyEngine(...) to create a ready-to-use value. + + +### `NewRegistry` + +```go +func NewRegistry() *Registry +``` + +NewRegistry creates an empty agent registry. +Usage: call NewRegistry(...) to create a ready-to-use value. \ No newline at end of file -- 2.45.3