agentbbs/internal/qryptinvite/config.go
Anthony Ettinger 97da723c5c feat(qrypt): qrypt.chat anonymous-invite issuer
AgentBBS becomes a trusted Ed25519 issuer for qrypt.chat anonymous
accounts. Members mint a signed, single-use qci1 token (per the shared
invite contract) that qrypt.chat verifies and redeems.

- internal/qryptinvite: Mint / GenerateIssuerKey / ParsePrivateKey +
  Config (AGENTBBS_QRYPT_* env) with unit tests (independent verify,
  payload assertions, jti uniqueness, tamper rejection, seed/full key).
- store: qrypt_invites table + QryptInviteCount / RecordQryptInvite
  (per-member quota, enforced in a tx; ErrQuotaExceeded) + test.
- plugins/qryptinvite: hub plugin (members only) — checks quota, mints,
  records, prints token + redeem URL.
- cmd/agentbbs: `qrypt-invite <user>` and `qrypt-issuer-keygen`
  subcommands wired into dispatch.
- setup.sh env template + docs/qrypt-invites.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:35:05 +00:00

74 lines
2.3 KiB
Go

package qryptinvite
import (
"crypto/ed25519"
"errors"
"os"
"strconv"
"time"
)
// Config is the resolved qrypt.chat invite-issuer configuration, read from the
// environment (see docs/qrypt-invites.md). It is shared by the SSH plugin and
// the ops CLI so they mint identical tokens.
type Config struct {
IssuerID string // AGENTBBS_QRYPT_ISSUER_ID (default "agentbbs")
Key string // AGENTBBS_QRYPT_ISSUER_KEY (base64 seed/priv)
TTL time.Duration // AGENTBBS_QRYPT_INVITE_TTL (default 168h)
RedeemURL string // AGENTBBS_QRYPT_REDEEM_URL (default https://qrypt.chat/anon?invite=)
Quota int // AGENTBBS_QRYPT_INVITE_QUOTA (default 5)
}
// DefaultIssuerID, DefaultRedeemURL, DefaultTTL and DefaultQuota are the
// fallbacks when the corresponding env var is unset.
const (
DefaultIssuerID = "agentbbs"
DefaultRedeemURL = "https://qrypt.chat/anon?invite="
DefaultTTL = 168 * time.Hour
DefaultQuota = 5
)
// ConfigFromEnv reads the AGENTBBS_QRYPT_* environment variables, applying
// defaults. The key is not validated here; call PrivateKey to parse it.
func ConfigFromEnv() Config {
c := Config{
IssuerID: DefaultIssuerID,
Key: os.Getenv("AGENTBBS_QRYPT_ISSUER_KEY"),
TTL: DefaultTTL,
RedeemURL: DefaultRedeemURL,
Quota: DefaultQuota,
}
if v := os.Getenv("AGENTBBS_QRYPT_ISSUER_ID"); v != "" {
c.IssuerID = v
}
if v := os.Getenv("AGENTBBS_QRYPT_REDEEM_URL"); v != "" {
c.RedeemURL = v
}
if v := os.Getenv("AGENTBBS_QRYPT_INVITE_TTL"); v != "" {
if d, err := time.ParseDuration(v); err == nil {
c.TTL = d
}
}
if v := os.Getenv("AGENTBBS_QRYPT_INVITE_QUOTA"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
c.Quota = n
}
}
return c
}
// ErrNoKey means AGENTBBS_QRYPT_ISSUER_KEY is unset, so no tokens can be minted.
var ErrNoKey = errors.New("qryptinvite: AGENTBBS_QRYPT_ISSUER_KEY is not set (run: agentbbs qrypt-issuer-keygen)")
// PrivateKey parses the configured issuer key, or returns ErrNoKey if unset.
func (c Config) PrivateKey() (ed25519.PrivateKey, error) {
if c.Key == "" {
return nil, ErrNoKey
}
return ParsePrivateKey(c.Key)
}
// RedeemLink returns the full URL a member opens to redeem token.
func (c Config) RedeemURLFor(token string) string {
return c.RedeemURL + token
}