refactor: upgrade core v0.8.0-alpha.1 and replace banned imports
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
87f87bfc0a
commit
f46cd04e2f
30 changed files with 485 additions and 339 deletions
47
auth/auth.go
47
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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():]
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
4
go.mod
4
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
|
||||
|
|
|
|||
6
go.sum
6
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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue