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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
# CodeRabbit Configuration File
# Reference: https://docs.coderabbit.ai/spec/configuration
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

language: "en-US"
tone_instruction: "Review code as a senior Go software engineer focusing on security practices, cryptographic hashing correctness, token lifecycles, and RBAC authorization."

reviews:
profile: "assertive"
tone_instructions: "Review code as a senior Go software engineer focusing on security practices, cryptographic hashing correctness, token lifecycles, and RBAC authorization."
auto_review:
enabled: true
drafts: false
high_level_summary: true
reviews_per_file: true
collapse_dependency_reviews: true

chat:
auto_reply: true
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,16 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Checkout auth
uses: actions/checkout@v4
- name: Checkout mdk
uses: actions/checkout@v4
Comment on lines +16 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ci.yml (lines 1-120) =="
sed -n '1,120p' .github/workflows/ci.yml | nl -ba

echo "== ci.yml (lines around 10-40) =="
sed -n '10,40p' .github/workflows/ci.yml | nl -ba

echo "== go.mod =="
cat go.mod | sed -n '1,200p' | nl -ba

echo "== Search for persist-credentials and checkout usage =="
rg -n "uses:\s*actions/checkout@|persist-credentials|GoHyperrr/mdk|ref:\s*\$\{\{|\bhead_ref\b|\bref_name\b" .github/workflows/ci.yml go.mod || true

echo "== Any other workflows with checkout? =="
rg -n "uses:\s*actions/checkout@" .github/workflows || true

Repository: GoHyperrr/auth

Length of output: 146


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflows tree =="
find .github/workflows -maxdepth 1 -type f -print || true

echo "== ci.yml =="
# show with line numbers using cat -n
cat -n .github/workflows/ci.yml | sed -n '1,140p'

echo "== go.mod (first 200 lines) =="
cat -n go.mod | sed -n '1,200p'

echo "== grep checkout + persist-credentials + GoHyperrr/mdk/ref derivation =="
rg -n "uses:\s*actions/checkout@|persist-credentials|GoHyperrr/mdk|ref:\s*\$\{\{|head_ref|ref_name" .github/workflows/ci.yml go.mod .github/workflows || true

echo "== exact checkout blocks (context) =="
rg -n "uses:\s*actions/checkout@" -C 3 .github/workflows/ci.yml .github/workflows || true

Repository: GoHyperrr/auth

Length of output: 3573


🌐 Web query:

actions/checkout persist-credentials default value

💡 Result:

The default value of the actions/checkout input persist-credentials is true.[1][2] This option controls whether the authentication token used to fetch the repository is persisted in the local git configuration so subsequent authenticated git commands work; it can be disabled with persist-credentials: false.[1][2]

Citations:


Pin actions/checkout to commit SHAs and disable persisted credentials (CI security).

  • In .github/workflows/ci.yml lines 16 and 18, both steps use mutable actions/checkout@v4; pin to the full commit SHA.
  • Neither checkout sets with: persist-credentials: false; actions/checkout defaults this to true, persisting the token in git config.
  • The GoHyperrr/mdk checkout uses ref: ${{ github.head_ref || github.ref_name }}, coupling the ref to the auth repo’s branch name and risking failures when that branch doesn’t exist in GoHyperrr/mdk.
Proposed fix
-      - name: Checkout auth
-        uses: actions/checkout@v4
+      - name: Checkout auth
+        uses: actions/checkout@<FULL_LENGTH_COMMIT_SHA_FOR_V4>
...
-      - name: Checkout mdk
-        uses: actions/checkout@v4
+      - name: Checkout mdk
+        uses: actions/checkout@<FULL_LENGTH_COMMIT_SHA_FOR_V4>
🧰 Tools
🪛 zizmor (1.25.2)

[error] 16-16: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 18-18: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 16 - 18, The CI workflow is using
mutable actions/checkout@v4 in two steps (including the "Checkout mdk" step) and
is not disabling credential persistence or safely setting the ref; update both
checkout steps to pin actions/checkout to the corresponding full commit SHAs
instead of `@v4`, add with: persist-credentials: false to prevent the token from
being written to git config, and change the "Checkout mdk" step to avoid
coupling to the auth repo branch by using a stable ref fallback (e.g., a
specific commit/branch or a safe fallback instead of github.head_ref ||
github.ref_name) so the checkout won't fail when the branch doesn't exist in
GoHyperrr/mdk.

Source: Linters/SAST tools

with:
repository: GoHyperrr/mdk
ref: ${{ github.head_ref || github.ref_name }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la .github/workflows || true
sed -n '1,120p' .github/workflows/ci.yml | nl -ba | sed -n '1,120p'
echo "----"
# show around line 21 specifically
nl -ba .github/workflows/ci.yml | sed -n '10,40p'
echo "----"
# find all occurrences of github.head_ref / ref_name / checkout repo usage
rg -n "github\.head_ref|\bref_name\b|checkout:" .github/workflows/ci.yml
echo "----"
# print checkout block(s) for context
rg -n "uses:\s*actions/checkout|GoHyperrr/mdk|repository:" .github/workflows/ci.yml -n

Repository: GoHyperrr/auth

Length of output: 283


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- workflows ----"
ls -la .github/workflows || true

echo "---- ci.yml (numbered via cat -n) ----"
cat -n .github/workflows/ci.yml | sed -n '1,120p'

echo "---- lines 15-35 ----"
sed -n '15,35p' .github/workflows/ci.yml

echo "---- checkout-related lines ----"
rg -n "actions/checkout|GoHyperrr/mdk|repository:|ref:\s*\$\{\{\s*github\.(head_ref|ref_name)\s*\|\|\s*github\.ref_name\s*\}\}" .github/workflows/ci.yml || true

echo "---- exact line containing github.head_ref/ref_name ----"
rg -n "github\.head_ref|\bgithub\.ref_name\b" .github/workflows/ci.yml || true

Repository: GoHyperrr/auth

Length of output: 2258


Make GoHyperrr/mdk checkout use a stable ref (PRs won’t have matching branch names)

In .github/workflows/ci.yml line 21, ref: ${{ github.head_ref || github.ref_name }} checks out GoHyperrr/mdk using the PR source branch name for pull_request events; that branch often won’t exist in GoHyperrr/mdk, causing the checkout to fail. Use a stable ref (e.g., main or a pinned commit/SHA) for deterministic CI.

Proposed fix
-          ref: ${{ github.head_ref || github.ref_name }}
+          ref: main
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ref: ${{ github.head_ref || github.ref_name }}
ref: main
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 21, The checkout for the external
repository GoHyperrr/mdk is using a dynamic ref expression (ref: ${{
github.head_ref || github.ref_name }}) which breaks for pull_request events
because the source branch may not exist in that repo; change the checkout step
that targets repository: GoHyperrr/mdk to use a stable ref (for example set ref
to "main" or a pinned commit SHA) so CI deterministically checks out a known
branch/commit instead of the PR source branch. Ensure you update the checkout
step where repository: GoHyperrr/mdk and ref is set (replace the expression with
the chosen stable ref).

path: mdk
- name: Move mdk to parent directory
run: mv mdk ../mdk
- uses: actions/setup-go@v5
with:
go-version: '1.25'
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Changelog

All notable changes to this project will be documented in this file.

## [0.1.0] - 2026-06-05

### Added
- Concrete auth.Actor persistence model implementing mdk.Actor interface.
Comment on lines +7 to +8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a blank line after the “Added” heading to satisfy markdownlint MD022.

Insert one empty line between Line 7 and Line 8 to resolve the documented lint warning.

Suggested patch
 ### Added
+
 - Concrete auth.Actor persistence model implementing mdk.Actor interface.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Added
- Concrete auth.Actor persistence model implementing mdk.Actor interface.
### Added
- Concrete auth.Actor persistence model implementing mdk.Actor interface.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 7-7: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 7 - 8, Markdown lint MD022 requires a blank line
after a heading; open CHANGELOG.md and insert a single empty line immediately
after the "### Added" heading (the line containing "### Added") so there is a
blank line between that heading and the following list item ("- Concrete
auth.Actor persistence model...").

Source: Linters/SAST tools

- Dynamic apikey and emailpass authentication providers.
- Decoupled command line runner integrations.
11 changes: 11 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Security Policy

## Supported Versions

For pre-1.0 releases we support the latest release or the latest patch series (e.g., 0.1.x), and for 1.0+ we support the latest major release branch.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify the supported versions policy.

The phrase "the latest release or the latest patch series" is ambiguous—readers may not understand whether you support only the newest patch (e.g., 0.1.3 but not 0.1.2) or all patches in the latest series (all 0.1.x). Similarly, "latest major release branch" for 1.0+ could be misread as supporting only one major version even after newer majors are released.

Consider rephrasing for clarity, for example:

  • "For pre-1.0 releases, we support all patch versions in the latest minor series (e.g., all 0.1.x patches). For 1.0+, we support all minor and patch versions in the latest major release."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SECURITY.md` at line 5, Replace the ambiguous sentence that reads "For
pre-1.0 releases we support the latest release or the latest patch series (e.g.,
0.1.x), and for 1.0+ we support the latest major release branch." with a clearer
policy statement: state that "For pre-1.0 releases we support all patch versions
in the latest minor series (e.g., all 0.1.x patches)." and that "For 1.0+ we
support all minor and patch versions within the latest major release (e.g., if
2.x is current, we support all 2.y.z)." Ensure the revised wording replaces the
original ambiguous sentence in SECURITY.md.


## Reporting a Vulnerability

If you discover a security vulnerability within Hyperrr or any of its modules, please do not disclose it publicly. Report it directly by emailing security@hyperrr.org or opening a draft security advisory on GitHub.

We will acknowledge receipt of your report within 48 hours and work with you to patch the issue promptly.
37 changes: 37 additions & 0 deletions actor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package auth

import (
"time"

"github.com/GoHyperrr/mdk"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== go.mod =="
sed -n '1,200p' go.mod

echo
echo "== mdk dependency / replace directives =="
rg -n -C2 'github\.com/GoHyperrr/mdk|^\s*replace\s+.*mdk' go.mod go.sum

Repository: GoHyperrr/auth

Length of output: 802


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== filesystem check: ../mdk =="
if [ -d ../mdk ]; then
  ls -la ../mdk | head -n 50
else
  echo "MISSING: ../mdk directory does not exist"
fi

echo
echo "== mdk references in go.sum =="
rg -n 'github\.com/GoHyperrr/mdk' go.sum || echo "No go.sum entries found for github.com/GoHyperrr/mdk"

Repository: GoHyperrr/auth

Length of output: 215


Fix broken local module replacement for github.com/GoHyperrr/mdk (currently replace ... => ../mdk).

go.mod replaces github.com/GoHyperrr/mdk with ../mdk, but ../mdk is not present in this checkout and there are no go.sum entries for the module, so builds that import it (via actor.go) cannot resolve dependencies. Remove/adjust the replace to use the published module version (or add the mdk source to the repo/CI checkout) and run go mod tidy.

🧰 Tools
🪛 GitHub Actions: CI / 0_build.txt

[error] 6-6: go vet failed: replacement directory ../mdk does not exist for module github.com/GoHyperrr/mdk@v0.0.0-20260605044506-3d2ab0d97ca9.

🪛 GitHub Actions: CI / build

[error] 6-6: go vet ./... failed: actor.go:6:2: github.com/GoHyperrr/mdk@v0.0.0-20260605044506-3d2ab0d97ca9: replacement directory ../mdk does not exist

🪛 GitHub Check: build

[failure] 6-6:
github.com/GoHyperrr/mdk@v0.0.0-20260605044506-3d2ab0d97ca9: replacement directory ../mdk does not exist

🪛 golangci-lint (2.12.2)

[error] 6-6: could not import github.com/GoHyperrr/mdk (actor.go:6:2: github.com/GoHyperrr/mdk@v0.0.0-20260605044506-3d2ab0d97ca9: replacement directory ../mdk does not exist)

(typecheck)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@actor.go` at line 6, actor.go imports the local-replaced module
"github.com/GoHyperrr/mdk" which fails because go.mod contains a replace to
../mdk that isn't in the checkout; fix by updating go.mod to remove or change
the replace for github.com/GoHyperrr/mdk to the published semantic version (or
ensure the ../mdk source is included in CI/checkout), then run `go mod tidy` to
regenerate go.sum and ensure imports in actor.go resolve cleanly.

Sources: Linters/SAST tools, Pipeline failures

)

// Actor is the concrete GORM database model representing a security principal.
// It implements the mdk.Actor interface.
type Actor struct {
ID string `gorm:"primaryKey" json:"id"`
Type mdk.ActorType `gorm:"index" json:"type"`
Name string `json:"name"`
Metadata mdk.JSONMap `gorm:"type:text" json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// Ensure Actor implements mdk.Actor.
var _ mdk.Actor = (*Actor)(nil)

func (a *Actor) GetID() string {
return a.ID
}

func (a *Actor) GetType() mdk.ActorType {
return a.Type
}

func (a *Actor) GetName() string {
return a.Name
}

func (a *Actor) GetMetadata() map[string]string {
return a.Metadata
}
11 changes: 6 additions & 5 deletions apikey/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import (
"fmt"
"time"

"github.com/GoHyperrr/auth"
"github.com/GoHyperrr/mdk"
"github.com/google/uuid"
)

// runAPIKeyCmd executes the CLI logic to generate a new API key.
func runAPIKeyCmd(rt mdk.Runtime, args []string) error {
// RunAPIKeyCmd executes the CLI logic to generate a new API key.
func RunAPIKeyCmd(rt mdk.Runtime, args []string) error {
if len(args) < 1 || args[0] != "generate" {
fmt.Println("Usage: hyperrr apikey generate")
return fmt.Errorf("invalid arguments")
Expand All @@ -23,16 +24,16 @@ func runAPIKeyCmd(rt mdk.Runtime, args []string) error {
}

// Auto-migrate tables locally to make sure Actors and APIKeys exist
err := database.AutoMigrate(&mdk.Actor{}, &APIKey{})
err := database.AutoMigrate(&auth.Actor{}, &APIKey{})
if err != nil {
return fmt.Errorf("failed to run migrations for apikey models: %w", err)
}

// Seed default MCP Developer Actor if not already present
var actorCount int64
database.Model(&mdk.Actor{}).Where("id = ?", "act_mcp_developer").Count(&actorCount)
database.Model(&auth.Actor{}).Where("id = ?", "act_mcp_developer").Count(&actorCount)
if actorCount == 0 {
devActor := mdk.Actor{
devActor := auth.Actor{
ID: "act_mcp_developer",
Type: mdk.ActorAIAgent,
Name: "Developer Agent",
Expand Down
6 changes: 3 additions & 3 deletions apikey/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (m *Module) CreateAPIKeyResolver(ctx context.Context, name string, expiresA
return nil, fmt.Errorf("unauthorized")
}

key, err := m.CreateAPIKey(ctx, actor.ID, name, expiresAt)
key, err := m.CreateAPIKey(ctx, actor.GetID(), name, expiresAt)
if err != nil {
return nil, err
}
Expand All @@ -52,7 +52,7 @@ func (m *Module) RevokeAPIKeyResolver(ctx context.Context, id string) (bool, err
return false, fmt.Errorf("unauthorized")
}

return m.RevokeAPIKey(ctx, actor.ID, id)
return m.RevokeAPIKey(ctx, actor.GetID(), id)
}

func (m *Module) ListAPIKeysResolver(ctx context.Context) ([]*APIKeyInfo, error) {
Expand All @@ -61,7 +61,7 @@ func (m *Module) ListAPIKeysResolver(ctx context.Context) ([]*APIKeyInfo, error)
return nil, fmt.Errorf("unauthorized")
}

keys, err := m.ListAPIKeys(ctx, actor.ID)
keys, err := m.ListAPIKeys(ctx, actor.GetID())
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion apikey/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

// GetActorByAPIKey retrieves an actor associated with a given API key.
func (m *Module) GetActorByAPIKey(ctx context.Context, key string) (*mdk.Actor, error) {
func (m *Module) GetActorByAPIKey(ctx context.Context, key string) (mdk.Actor, error) {
var apiKey APIKey
err := m.database.WithContext(ctx).Preload("Actor").First(&apiKey, "key = ?", key).Error
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions apikey/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package apikey
import (
"time"

"github.com/GoHyperrr/mdk"
"github.com/GoHyperrr/auth"
"gorm.io/gorm"
)

Expand All @@ -13,7 +13,7 @@ type APIKey struct {
Name string `gorm:"default:'';not null" json:"name"`
Key string `gorm:"uniqueIndex;not null" json:"key"`
ActorID string `gorm:"not null" json:"actor_id"`
Actor mdk.Actor `gorm:"foreignKey:ActorID" json:"actor"`
Actor auth.Actor `gorm:"foreignKey:ActorID" json:"actor"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Expand Down
10 changes: 0 additions & 10 deletions apikey/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,5 @@ func init() {
mdk.Register(func() mdk.Module {
return NewModule()
})

mdk.RegisterCommand(mdk.CLICommand{
Group: "auth",
Name: "apikey",
Usage: "generate",
Short: "Generate a new secure API key on-demand",
Long: "Generate a new secure API key on-demand and write it to the database.",
NeedsDB: true,
Run: runAPIKeyCmd,
})
}

9 changes: 5 additions & 4 deletions emailpass/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"fmt"
"strings"

"github.com/GoHyperrr/auth"
"github.com/GoHyperrr/mdk"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)

// runEmailPassCmd registers a new user via CLI.
func runEmailPassCmd(rt mdk.Runtime, args []string) error {
// RunEmailPassCmd registers a new user via CLI.
func RunEmailPassCmd(rt mdk.Runtime, args []string) error {
if len(args) < 4 || args[0] != "register" {
fmt.Println("Usage: hyperrr emailpass register <email> <password> <name>")
return fmt.Errorf("invalid arguments")
Expand All @@ -27,7 +28,7 @@ func runEmailPassCmd(rt mdk.Runtime, args []string) error {
}

// Auto-migrate tables locally to ensure Actor and User exist
err := database.AutoMigrate(&mdk.Actor{}, &User{})
err := database.AutoMigrate(&auth.Actor{}, &User{})
if err != nil {
return fmt.Errorf("failed to run migrations for emailpass models: %w", err)
}
Expand All @@ -45,7 +46,7 @@ func runEmailPassCmd(rt mdk.Runtime, args []string) error {
}

actorID := "act_" + uuid.New().String()
actor := mdk.Actor{
actor := auth.Actor{
ID: actorID,
Type: mdk.ActorHuman,
Name: name,
Expand Down
6 changes: 3 additions & 3 deletions emailpass/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (m *Module) RegisterResolver(ctx context.Context, email string, password st
return nil, err
}

token, err := m.store.GenerateToken(*actor)
token, err := m.store.GenerateToken(actor)
if err != nil {
return nil, fmt.Errorf("failed to generate token: %w", err)
}
Expand All @@ -47,7 +47,7 @@ func (m *Module) LoginResolver(ctx context.Context, email string, password strin
return nil, err
}

token, err := m.store.GenerateToken(*actor)
token, err := m.store.GenerateToken(actor)
if err != nil {
return nil, fmt.Errorf("failed to generate token: %w", err)
}
Expand All @@ -58,7 +58,7 @@ func (m *Module) LoginResolver(ctx context.Context, email string, password strin
}, nil
}

func (m *Module) Me(ctx context.Context) (*mdk.Actor, error) {
func (m *Module) Me(ctx context.Context) (mdk.Actor, error) {
actor, ok := mdk.ActorFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
Expand Down
15 changes: 8 additions & 7 deletions emailpass/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"

"github.com/GoHyperrr/auth"
"github.com/GoHyperrr/mdk"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
Expand All @@ -27,20 +28,20 @@ func (m *Module) ValidateActor(ctx context.Context, input any) (any, error) {
return nil, fmt.Errorf("actor_id is required")
}

var actor mdk.Actor
var actor auth.Actor
if err := m.database.First(&actor, "id = ?", actorID).Error; err != nil {
return nil, fmt.Errorf("actor not found: %w", err)
}

return map[string]any{
"id": actor.ID,
"type": actor.Type,
"name": actor.Name,
"id": actor.GetID(),
"type": actor.GetType(),
"name": actor.GetName(),
}, nil
}

// Register creates a new user and actor.
func (m *Module) Register(ctx context.Context, email, password, name string) (*mdk.Actor, error) {
func (m *Module) Register(ctx context.Context, email, password, name string) (mdk.Actor, error) {
if email == "" || password == "" || name == "" {
return nil, fmt.Errorf("email, password, and name are required")
}
Expand All @@ -50,7 +51,7 @@ func (m *Module) Register(ctx context.Context, email, password, name string) (*m
}

actorID := "act_" + uuid.New().String()
actor := mdk.Actor{
actor := auth.Actor{
ID: actorID,
Type: mdk.ActorHuman,
Name: name,
Expand Down Expand Up @@ -89,7 +90,7 @@ func (m *Module) Register(ctx context.Context, email, password, name string) (*m
}

// Login verifies credentials and returns the actor.
func (m *Module) Login(ctx context.Context, email, password string) (*mdk.Actor, error) {
func (m *Module) Login(ctx context.Context, email, password string) (mdk.Actor, error) {
var user User
if err := m.database.Preload("Actor").First(&user, "email = ?", email).Error; err != nil {
return nil, fmt.Errorf("invalid credentials")
Expand Down
7 changes: 4 additions & 3 deletions emailpass/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package emailpass
import (
"time"

"github.com/GoHyperrr/auth"
"github.com/GoHyperrr/mdk"
"gorm.io/gorm"
)
Expand All @@ -13,15 +14,15 @@ type User struct {
Email string `gorm:"uniqueIndex;not null" json:"email"`
PasswordHash string `gorm:"not null" json:"-"`
ActorID string `gorm:"not null" json:"actor_id"`
Actor mdk.Actor `gorm:"foreignKey:ActorID" json:"actor"`
Actor auth.Actor `gorm:"foreignKey:ActorID" json:"actor"`
Comment on lines 16 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Files containing emailpass/model.go and emailpass/handlers.go =="
ls -la emailpass || true
git ls-files | rg -n '^emailpass/(model\.go|handlers\.go)$' || true

echo
echo "== emailpass/model.go =="
sed -n '1,120p' emailpass/model.go

echo
echo "== emailpass/handlers.go (or similarly named handlers) =="
if [ -f emailpass/handlers.go ]; then
  sed -n '1,240p' emailpass/handlers.go
else
  git ls-files | rg '^emailpass/.*handlers\.go$' || true
fi

echo
echo "== Search for Preload(\"Actor\") and references to user.Actor / ActorID =="
rg -n --hidden --no-ignore-vcs 'Preload\("Actor"\)|Preload\(\"Actor\"\)|\.Actor\b|GenerateToken|Login\(' emailpass || true

echo
echo "== Search for GenerateToken implementation =="
rg -n --hidden --no-ignore-vcs 'func .*GenerateToken|GenerateToken\(' . || true

Repository: GoHyperrr/auth

Length of output: 4700


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== emailpass/graphql.go =="
sed -n '1,220p' emailpass/graphql.go

echo
echo "== jwt/jwt.go =="
if [ -f jwt/jwt.go ]; then
  sed -n '1,220p' jwt/jwt.go
else
  git ls-files | rg -n '^jwt/.*\.go$' || true
fi

echo
echo "== emailpass/module.go =="
sed -n '1,240p' emailpass/module.go

echo
echo "== Search: returns &user.Actor / user.Actor usage =="
rg -n --hidden --no-ignore-vcs '&user\.Actor|\buser\.Actor\b' emailpass || true

echo
echo "== Search: actor_id constraints / foreign keys =="
rg -n --hidden --no-ignore-vcs 'ActorID|foreignKey|constraint|OnDelete|references' emailpass || true

Repository: GoHyperrr/auth

Length of output: 9363


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find auth.Actor type =="
git ls-files | rg -n '^.*auth/.*\.go$' || true
rg -n --hidden --no-ignore-vcs 'type Actor struct|func \(.*\) GetID\(\)|func \(.*\) GetType\(\)' . || true

echo
echo "== Read auth.Actor definition =="
# Try likely filenames
for f in $(git ls-files | rg '^auth/.*\.go$' || true); do
  rg -n --hidden --no-ignore-vcs 'type Actor struct' "$f" >/dev/null 2>&1 && echo "$f"
done | head -n 20

# Print the first match file for Actor struct
actor_file="$(git ls-files | rg '^auth/.*\.go$' | while read -r f; do rg -l --hidden --no-ignore-vcs 'type Actor struct' "$f" >/dev/null && echo "$f"; done | head -n 1)"
if [ -n "${actor_file:-}" ] && [ -f "$actor_file" ]; then
  echo
  echo "== $actor_file (snippet) =="
  nl -ba "$actor_file" | sed -n '1,220p'
else
  echo "Could not locate auth/Actor struct file under auth/ directory."
fi

echo
echo "== Find where validate_actor workflow step is used =="
rg -n --hidden --no-ignore-vcs '"auth\.validate_actor"|validate_actor' . || true

echo
echo "== Search for token validation pipeline in runtime =="
rg -n --hidden --no-ignore-vcs 'ValidateToken|TokenValidator|Authenticate|auth\.emailpass' . || true

Repository: GoHyperrr/auth

Length of output: 256


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== actor.go =="
nl -ba actor.go | sed -n '1,120p'

echo
echo "== Search for Login / GenerateToken usage in auth flow =="
rg -n --hidden --no-ignore-vcs 'LoginResolver|Store\(\)\.GenerateToken|GenerateToken\(|ValidateToken\(|TokenValidator|Authenticate|auth\.emailpass|emailpass\.Login|emailpass\.LoginResolver' . || true

echo
echo "== Search for mdk runtime authentication / middleware hook =="
rg -n --hidden --no-ignore-vcs 'Authenticate|token.*validate|ValidateToken|TokenValidator' . || true

Repository: GoHyperrr/auth

Length of output: 134


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== actor.go =="
cat -n actor.go | sed -n '1,140p'

echo
echo "== Search: Login/GenerateToken call sites =="
rg -n --hidden --no-ignore-vcs 'emailpass/|LoginResolver|emailpass\.Login|GenerateToken\(|GenerateToken\(' . || true

echo
echo "== Search: token validation / auth middleware =="
rg -n --hidden --no-ignore-vcs 'ValidateToken|TokenValidator|Authenticate|auth\.emailpass' . || true

echo
echo "== Search: where ValidateToken is called =="
rg -n --hidden --no-ignore-vcs '\.ValidateToken\(' . || true

Repository: GoHyperrr/auth

Length of output: 2665


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for validate_actor usage =="
rg -n --hidden --no-ignore-vcs 'validate_actor|ValidateActorStep|auth\.validate_actor' . || true

echo
echo "== Search for checks on empty actor_id / ActorID =="
rg -n --hidden --no-ignore-vcs 'ActorID.*(==|!=|empty|""|not null)|actor_id.*(==|!=|empty|"" )|claims\.ActorID' . || true

echo
echo "== Search for GetID() validation =="
rg -n --hidden --no-ignore-vcs '\.GetID\(\)' . || true

echo
echo "== Search for JWT claim parsing usage =="
rg -n --hidden --no-ignore-vcs 'ActorType|ActorID|RegisteredClaims|claims\.' jwt . emailpass . || true

Repository: GoHyperrr/auth

Length of output: 5205


Fix emailpass.User.Actor association to prevent issuing JWTs with empty actor_id/type

User.Actor is a non-pointer auth.Actor (value association). If Preload("Actor") can’t find the related row, GORM leaves user.Actor zero-valued; emailpass/handlers.go Login still returns &user.Actor, and GenerateToken then uses empty actor_id/actor_type when minting the JWT.

Suggested direction
 type User struct {
   ID           string         `gorm:"primaryKey" json:"id"`
   Email        string         `gorm:"uniqueIndex;not null" json:"email"`
   PasswordHash string         `gorm:"not null" json:"-"`
   ActorID      string         `gorm:"not null" json:"actor_id"`
-  Actor        auth.Actor     `gorm:"foreignKey:ActorID" json:"actor"`
+  Actor        *auth.Actor    `gorm:"foreignKey:ActorID" json:"actor"`
   CreatedAt    time.Time      `json:"created_at"`
   UpdatedAt    time.Time      `json:"updated_at"`
   DeletedAt    gorm.DeletedAt `gorm:"index" json:"-"`
 }

Also reject login when the preloaded actor is nil (or when user.Actor.ID != user.ActorID) before returning it.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
type User struct {
	ID           string         `gorm:"primaryKey" json:"id"`
	Email        string         `gorm:"uniqueIndex;not null" json:"email"`
	PasswordHash string         `gorm:"not null" json:"-"`
	ActorID      string         `gorm:"not null" json:"actor_id"`
	Actor        *auth.Actor    `gorm:"foreignKey:ActorID" json:"actor"`
	CreatedAt    time.Time      `json:"created_at"`
	UpdatedAt    time.Time      `json:"updated_at"`
	DeletedAt    gorm.DeletedAt `gorm:"index" json:"-"`
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@emailpass/model.go` around lines 16 - 17, User.Actor is a value (non-pointer)
association so when Preload("Actor") finds no row it stays zero-value and Login
returns &user.Actor which lets GenerateToken mint JWTs with empty actor_id/type;
change User.Actor to a pointer (auth.Actor) and update the GORM tag to match
(e.g., Actor *auth.Actor `gorm:"foreignKey:ActorID"`), then in the Login handler
(emailpass/handlers.go) add a check that user.Actor != nil and user.Actor.ID ==
user.ActorID (or reject) before returning the user so GenerateToken never
receives an empty/mismatched actor.

CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

type AuthResponse struct {
Token string `json:"token"`
Actor *mdk.Actor `json:"actor"`
Token string `json:"token"`
Actor mdk.Actor `json:"actor"`
}


12 changes: 1 addition & 11 deletions emailpass/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (m *Module) Routes() []mdk.Route {
}

// ValidateToken implements mdk.TokenValidator interface.
func (m *Module) ValidateToken(ctx context.Context, token string) (*mdk.Actor, error) {
func (m *Module) ValidateToken(ctx context.Context, token string) (mdk.Actor, error) {
return m.store.ValidateToken(ctx, token)
}

Expand Down Expand Up @@ -144,15 +144,5 @@ func init() {
mdk.Register(func() mdk.Module {
return NewModule("", "")
})

mdk.RegisterCommand(mdk.CLICommand{
Group: "auth",
Name: "user",
Usage: "register <email> <password> <name>",
Short: "Register a new user via email/password",
Long: "Register a new user dynamically via email/password and write it to the database.",
NeedsDB: true,
Run: runEmailPassCmd,
})
}

2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/text v0.37.0 // indirect
)

replace github.com/GoHyperrr/mdk => ../mdk

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove committed local replace for mdk to avoid non-reproducible builds.

Line 19 pins github.com/GoHyperrr/mdk to a local ../mdk path. This commonly breaks CI and contributor builds when that sibling path is missing. Keep module resolution on the required version in go.mod and use go work for local linking instead.

Suggested change
-replace github.com/GoHyperrr/mdk => ../mdk
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
replace github.com/GoHyperrr/mdk => ../mdk
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go.mod` at line 19, The go.mod contains a local replace directive "replace
github.com/GoHyperrr/mdk => ../mdk" which forces a sibling path and breaks CI;
remove that replace line so the module resolves via the declared version, and if
local development linking is needed use a go.work file instead; locate and
delete the replace directive referencing github.com/GoHyperrr/mdk (and update
any developer docs to suggest go work for local overrides).

2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
github.com/GoHyperrr/mdk v0.0.0-20260605044506-3d2ab0d97ca9 h1:32yHu/Gnk+ztrDUZ8D+WnjWmkKZd8ebs+yxrjKJFq8Y=
github.com/GoHyperrr/mdk v0.0.0-20260605044506-3d2ab0d97ca9/go.mod h1:eZBBN0St+r0Gu5K5CklIsS6/SlVx6t2WsvOjkR1t4Ak=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand Down
Loading
Loading