feat: module scaffold and digest normalize/guard

pkg/digest (not internal/) so Task 2 pkg/tsnats can import helpers.
This commit is contained in:
George Lambert 2026-09-29 11:43:22 -04:00
commit e4432fc555
5 changed files with 150 additions and 2 deletions

3
.gitignore vendored
View file

@ -1,4 +1,5 @@
.worktrees/
.superpowers/
bin/
*.exe
.venv/

View file

@ -22,7 +22,7 @@ cd verae-mcp-timestamping-server-go
go build -o bin/verae-mcp-timestamping-server ./cmd/verae-mcp-timestamping-server
```
(Requires Go 1.22+ once the module is scaffolded. Design: `docs/superpowers/specs/2026-09-29-go-mcp-timestamping-server-design.md`.)
Requires Go 1.22+. Design: `docs/superpowers/specs/2026-09-29-go-mcp-timestamping-server-design.md`.
## Usage

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module git.georgelambert.org/marchon/verae-mcp-timestamping-server-go
go 1.22

72
pkg/digest/digest.go Normal file
View file

@ -0,0 +1,72 @@
package digest
import (
"fmt"
"regexp"
"sort"
"strings"
)
// ForbiddenKeys are argument names rejected by digests-only tools.
var ForbiddenKeys = []string{
"content",
"bytes",
"file",
"data",
"path",
"body",
"payload",
"blob",
"archive",
}
var (
hex64 = regexp.MustCompile(`^[0-9a-f]{64}$`)
forbiddenLower = func() map[string]struct{} {
m := make(map[string]struct{}, len(ForbiddenKeys))
for _, k := range ForbiddenKeys {
m[strings.ToLower(k)] = struct{}{}
}
return m
}()
)
// NormalizeSHA256 strips an optional "sha256:" prefix, lowercases, and
// requires exactly 64 hexadecimal characters.
func NormalizeSHA256(s string) (string, error) {
raw := strings.TrimSpace(s)
if strings.HasPrefix(strings.ToLower(raw), "sha256:") {
raw = strings.TrimSpace(raw[len("sha256:"):])
}
raw = strings.ToLower(raw)
if !hex64.MatchString(raw) {
return "", fmt.Errorf("sha256 must be 64 hexadecimal characters")
}
return raw, nil
}
// AssertDigestOnly rejects maps that include any ForbiddenKeys (case-insensitive).
func AssertDigestOnly(args map[string]any) error {
if args == nil {
return nil
}
var bad []string
seen := make(map[string]struct{})
for k := range args {
lk := strings.ToLower(k)
if _, ok := forbiddenLower[lk]; ok {
if _, dup := seen[lk]; !dup {
seen[lk] = struct{}{}
bad = append(bad, k)
}
}
}
if len(bad) == 0 {
return nil
}
sort.Strings(bad)
return fmt.Errorf(
"client refuses to send content/bytes to stamp/verify/hash_report; got: %s",
strings.Join(bad, ", "),
)
}

72
pkg/digest/digest_test.go Normal file
View file

@ -0,0 +1,72 @@
package digest
import (
"strings"
"testing"
)
func TestNormalizeSHA256_StripPrefix(t *testing.T) {
h := strings.Repeat("a", 64)
got, err := NormalizeSHA256("sha256:" + h)
if err != nil || got != h {
t.Fatalf("got %q err %v", got, err)
}
}
func TestNormalizeSHA256_LowerHex(t *testing.T) {
h := strings.Repeat("A", 64)
want := strings.Repeat("a", 64)
got, err := NormalizeSHA256(h)
if err != nil || got != want {
t.Fatalf("got %q err %v", got, err)
}
}
func TestNormalizeSHA256_RejectsShort(t *testing.T) {
_, err := NormalizeSHA256("abcd")
if err == nil {
t.Fatal("expected error")
}
}
func TestNormalizeSHA256_MixedCasePrefix(t *testing.T) {
h := strings.Repeat("b", 64)
got, err := NormalizeSHA256("SHA256:" + h)
if err != nil || got != h {
t.Fatalf("got %q err %v", got, err)
}
}
func TestAssertDigestOnly_RejectsContent(t *testing.T) {
err := AssertDigestOnly(map[string]any{"sha256": strings.Repeat("a", 64), "content": "x"})
if err == nil {
t.Fatal("expected error")
}
}
func TestAssertDigestOnly_CaseInsensitive(t *testing.T) {
err := AssertDigestOnly(map[string]any{"CONTENT": "x"})
if err == nil {
t.Fatal("expected error")
}
}
func TestAssertDigestOnly_OK(t *testing.T) {
err := AssertDigestOnly(map[string]any{"sha256": strings.Repeat("a", 64), "scope": "local"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestForbiddenKeys_ContainsContent(t *testing.T) {
found := false
for _, k := range ForbiddenKeys {
if k == "content" {
found = true
break
}
}
if !found {
t.Fatalf("ForbiddenKeys missing content: %v", ForbiddenKeys)
}
}