Enhance Directory Client Library for NIP-XX Protocol
- Introduced a TypeScript client library for the Distributed Directory Consensus Protocol (NIP-XX), providing a high-level API for managing directory events, identity resolution, and trust calculations. - Implemented core functionalities including event parsing, trust score aggregation, and replication filtering, mirroring the Go implementation. - Added comprehensive documentation and development guides for ease of use and integration. - Updated the `.gitignore` to include additional dependencies and build artifacts for the TypeScript client. - Enhanced validation mechanisms for group tag names and trust levels, ensuring robust input handling and security. - Created a new `bun.lock` file to manage package dependencies effectively.
This commit is contained in:
@@ -2,6 +2,7 @@ package directory
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/errorf"
|
||||
@@ -18,7 +19,49 @@ type GroupTagAct struct {
|
||||
TagValue string
|
||||
Actor string
|
||||
Confidence int
|
||||
Owners *OwnershipSpec
|
||||
Created *time.Time
|
||||
Description string
|
||||
IdentityTag *IdentityTag
|
||||
}
|
||||
|
||||
// OwnershipSpec defines the ownership control for a group tag.
|
||||
type OwnershipSpec struct {
|
||||
Scheme SignatureScheme
|
||||
Owners []string // Public keys of owners
|
||||
}
|
||||
|
||||
// SignatureScheme defines the type of signature requirement.
|
||||
type SignatureScheme string
|
||||
|
||||
const (
|
||||
SchemeSingle SignatureScheme = "single"
|
||||
Scheme2of3 SignatureScheme = "2-of-3"
|
||||
Scheme3of5 SignatureScheme = "3-of-5"
|
||||
)
|
||||
|
||||
// ValidateSignatureScheme checks if a signature scheme is valid.
|
||||
func ValidateSignatureScheme(scheme SignatureScheme) error {
|
||||
switch scheme {
|
||||
case SchemeSingle, Scheme2of3, Scheme3of5:
|
||||
return nil
|
||||
default:
|
||||
return errorf.E("invalid signature scheme: %s", scheme)
|
||||
}
|
||||
}
|
||||
|
||||
// RequiredSignatures returns the number of signatures required for the scheme.
|
||||
func (s SignatureScheme) RequiredSignatures() int {
|
||||
switch s {
|
||||
case SchemeSingle:
|
||||
return 1
|
||||
case Scheme2of3:
|
||||
return 2
|
||||
case Scheme3of5:
|
||||
return 3
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// NewGroupTagAct creates a new Group Tag Act event.
|
||||
@@ -26,7 +69,9 @@ func NewGroupTagAct(
|
||||
pubkey []byte,
|
||||
groupID, tagName, tagValue, actor string,
|
||||
confidence int,
|
||||
owners *OwnershipSpec,
|
||||
description string,
|
||||
identityTag *IdentityTag,
|
||||
) (gta *GroupTagAct, err error) {
|
||||
|
||||
// Validate required fields
|
||||
@@ -36,6 +81,12 @@ func NewGroupTagAct(
|
||||
if groupID == "" {
|
||||
return nil, errorf.E("group ID is required")
|
||||
}
|
||||
|
||||
// Validate group ID is URL-safe
|
||||
if err = ValidateGroupTagName(groupID); chk.E(err) {
|
||||
return nil, errorf.E("invalid group ID: %w", err)
|
||||
}
|
||||
|
||||
if tagName == "" {
|
||||
return nil, errorf.E("tag name is required")
|
||||
}
|
||||
@@ -52,6 +103,31 @@ func NewGroupTagAct(
|
||||
return nil, errorf.E("confidence must be between 0 and 100")
|
||||
}
|
||||
|
||||
// Validate ownership spec if provided
|
||||
if owners != nil {
|
||||
if err = ValidateSignatureScheme(owners.Scheme); chk.E(err) {
|
||||
return
|
||||
}
|
||||
if len(owners.Owners) == 0 {
|
||||
return nil, errorf.E("at least one owner is required")
|
||||
}
|
||||
// Validate owner count matches scheme
|
||||
switch owners.Scheme {
|
||||
case SchemeSingle:
|
||||
if len(owners.Owners) != 1 {
|
||||
return nil, errorf.E("single scheme requires exactly 1 owner")
|
||||
}
|
||||
case Scheme2of3:
|
||||
if len(owners.Owners) != 3 {
|
||||
return nil, errorf.E("2-of-3 scheme requires exactly 3 owners")
|
||||
}
|
||||
case Scheme3of5:
|
||||
if len(owners.Owners) != 5 {
|
||||
return nil, errorf.E("3-of-5 scheme requires exactly 5 owners")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create base event
|
||||
ev := CreateBaseEvent(pubkey, GroupTagActKind)
|
||||
ev.Content = []byte(description)
|
||||
@@ -62,6 +138,26 @@ func NewGroupTagAct(
|
||||
ev.Tags.Append(tag.NewFromAny(string(ActorTag), actor))
|
||||
ev.Tags.Append(tag.NewFromAny(string(ConfidenceTag), strconv.Itoa(confidence)))
|
||||
|
||||
// Add ownership tag if provided
|
||||
if owners != nil {
|
||||
ownersTagParts := make([]any, 0, len(owners.Owners)+2)
|
||||
ownersTagParts = append(ownersTagParts, string(OwnersTag), string(owners.Scheme))
|
||||
for _, owner := range owners.Owners {
|
||||
ownersTagParts = append(ownersTagParts, owner)
|
||||
}
|
||||
ev.Tags.Append(tag.NewFromAny(ownersTagParts...))
|
||||
}
|
||||
|
||||
// Add created timestamp
|
||||
created := time.Now()
|
||||
ev.Tags.Append(tag.NewFromAny(string(CreatedTag), strconv.FormatInt(created.Unix(), 10)))
|
||||
|
||||
// Add identity tag if provided
|
||||
if identityTag != nil {
|
||||
iTag := tag.NewFromAny(string(ITag), identityTag.NPubIdentity, identityTag.Nonce, identityTag.Signature)
|
||||
ev.Tags.Append(iTag)
|
||||
}
|
||||
|
||||
gta = &GroupTagAct{
|
||||
Event: ev,
|
||||
GroupID: groupID,
|
||||
@@ -69,7 +165,10 @@ func NewGroupTagAct(
|
||||
TagValue: tagValue,
|
||||
Actor: actor,
|
||||
Confidence: confidence,
|
||||
Owners: owners,
|
||||
Created: &created,
|
||||
Description: description,
|
||||
IdentityTag: identityTag,
|
||||
}
|
||||
|
||||
return
|
||||
@@ -124,6 +223,44 @@ func ParseGroupTagAct(ev *event.E) (gta *GroupTagAct, err error) {
|
||||
return nil, errorf.E("confidence must be between 0 and 100")
|
||||
}
|
||||
|
||||
// Parse optional ownership tag
|
||||
var owners *OwnershipSpec
|
||||
ownersTag := ev.Tags.GetFirst(OwnersTag)
|
||||
if ownersTag != nil && ownersTag.Len() >= 3 {
|
||||
scheme := SignatureScheme(ownersTag.T[1])
|
||||
if err = ValidateSignatureScheme(scheme); chk.E(err) {
|
||||
return nil, errorf.E("invalid signature scheme: %w", err)
|
||||
}
|
||||
ownerPubkeys := make([]string, 0, ownersTag.Len()-2)
|
||||
for i := 2; i < ownersTag.Len(); i++ {
|
||||
ownerPubkeys = append(ownerPubkeys, string(ownersTag.T[i]))
|
||||
}
|
||||
owners = &OwnershipSpec{
|
||||
Scheme: scheme,
|
||||
Owners: ownerPubkeys,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse optional created timestamp
|
||||
var created *time.Time
|
||||
createdTag := ev.Tags.GetFirst(CreatedTag)
|
||||
if createdTag != nil {
|
||||
var timestamp int64
|
||||
if timestamp, err = strconv.ParseInt(string(createdTag.Value()), 10, 64); err == nil {
|
||||
t := time.Unix(timestamp, 0)
|
||||
created = &t
|
||||
}
|
||||
}
|
||||
|
||||
// Parse optional identity tag
|
||||
var identityTag *IdentityTag
|
||||
iTag := ev.Tags.GetFirst(ITag)
|
||||
if iTag != nil {
|
||||
if identityTag, err = ParseIdentityTag(iTag); chk.E(err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
gta = &GroupTagAct{
|
||||
Event: ev,
|
||||
GroupID: string(dTag.Value()),
|
||||
@@ -131,7 +268,10 @@ func ParseGroupTagAct(ev *event.E) (gta *GroupTagAct, err error) {
|
||||
TagValue: string(groupTagTag.T[2]),
|
||||
Actor: string(actorTag.Value()),
|
||||
Confidence: confidence,
|
||||
Owners: owners,
|
||||
Created: created,
|
||||
Description: string(ev.Content),
|
||||
IdentityTag: identityTag,
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -168,19 +168,22 @@ func (tc *TrustCalculator) GetTrustLevel(pubkey string) TrustLevel {
|
||||
return act.GetTrustLevel()
|
||||
}
|
||||
}
|
||||
return TrustLevel("")
|
||||
return TrustLevelNone // Return 0 for no trust
|
||||
}
|
||||
|
||||
// CalculateInheritedTrust calculates inherited trust through the web of trust.
|
||||
// With numeric trust levels, inherited trust is calculated by multiplying
|
||||
// the trust percentages at each hop, reducing trust over distance.
|
||||
func (tc *TrustCalculator) CalculateInheritedTrust(
|
||||
fromPubkey, toPubkey string,
|
||||
) TrustLevel {
|
||||
// Direct trust
|
||||
if directTrust := tc.GetTrustLevel(toPubkey); directTrust != "" {
|
||||
if directTrust := tc.GetTrustLevel(toPubkey); directTrust > 0 {
|
||||
return directTrust
|
||||
}
|
||||
|
||||
// Look for inherited trust through intermediate nodes
|
||||
var maxInheritedTrust TrustLevel = 0
|
||||
for intermediatePubkey, act := range tc.acts {
|
||||
if act.IsExpired() {
|
||||
continue
|
||||
@@ -188,43 +191,35 @@ func (tc *TrustCalculator) CalculateInheritedTrust(
|
||||
|
||||
// Check if we trust the intermediate node
|
||||
intermediateLevel := tc.GetTrustLevel(intermediatePubkey)
|
||||
if intermediateLevel == "" {
|
||||
if intermediateLevel == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if intermediate node trusts the target
|
||||
targetLevel := tc.GetTrustLevel(toPubkey)
|
||||
if targetLevel == "" {
|
||||
if targetLevel == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate inherited trust level
|
||||
return tc.combinesTrustLevels(intermediateLevel, targetLevel)
|
||||
// Calculate inherited trust level (multiply percentages)
|
||||
inheritedLevel := tc.combinesTrustLevels(intermediateLevel, targetLevel)
|
||||
if inheritedLevel > maxInheritedTrust {
|
||||
maxInheritedTrust = inheritedLevel
|
||||
}
|
||||
}
|
||||
|
||||
return TrustLevel("")
|
||||
return maxInheritedTrust
|
||||
}
|
||||
|
||||
// combinesTrustLevels combines two trust levels to calculate inherited trust.
|
||||
// With numeric trust levels (0-100), inherited trust is calculated by
|
||||
// multiplying the two percentages: (level1 * level2) / 100
|
||||
// This naturally reduces trust over distance.
|
||||
func (tc *TrustCalculator) combinesTrustLevels(level1, level2 TrustLevel) TrustLevel {
|
||||
// Trust inheritance rules:
|
||||
// high + high = medium
|
||||
// high + medium = low
|
||||
// medium + medium = low
|
||||
// anything else = no trust
|
||||
|
||||
if level1 == TrustLevelHigh && level2 == TrustLevelHigh {
|
||||
return TrustLevelMedium
|
||||
}
|
||||
if (level1 == TrustLevelHigh && level2 == TrustLevelMedium) ||
|
||||
(level1 == TrustLevelMedium && level2 == TrustLevelHigh) {
|
||||
return TrustLevelLow
|
||||
}
|
||||
if level1 == TrustLevelMedium && level2 == TrustLevelMedium {
|
||||
return TrustLevelLow
|
||||
}
|
||||
|
||||
return TrustLevel("")
|
||||
// Multiply percentages: (level1% * level2%) = (level1 * level2) / 100
|
||||
// Example: 75% trust * 50% trust = 37.5% inherited trust
|
||||
combined := (uint16(level1) * uint16(level2)) / 100
|
||||
return TrustLevel(combined)
|
||||
}
|
||||
|
||||
// ReplicationFilter helps determine which events should be replicated.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package directory
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -53,7 +54,7 @@ func NewTrustAct(
|
||||
if len(targetPubkey) != 64 {
|
||||
return nil, errorf.E("target pubkey must be 64 hex characters")
|
||||
}
|
||||
if err = ValidateTrustLevel(string(trustLevel)); chk.E(err) {
|
||||
if err = ValidateTrustLevel(trustLevel); chk.E(err) {
|
||||
return
|
||||
}
|
||||
if relayURL == "" {
|
||||
@@ -65,7 +66,7 @@ func NewTrustAct(
|
||||
|
||||
// Add required tags
|
||||
ev.Tags.Append(tag.NewFromAny(string(PubkeyTag), targetPubkey))
|
||||
ev.Tags.Append(tag.NewFromAny(string(TrustLevelTag), string(trustLevel)))
|
||||
ev.Tags.Append(tag.NewFromAny(string(TrustLevelTag), strconv.FormatUint(uint64(trustLevel), 10)))
|
||||
ev.Tags.Append(tag.NewFromAny(string(RelayTag), relayURL))
|
||||
|
||||
// Add optional expiry
|
||||
@@ -142,8 +143,12 @@ func ParseTrustAct(ev *event.E) (ta *TrustAct, err error) {
|
||||
}
|
||||
|
||||
// Validate trust level
|
||||
trustLevel := TrustLevel(trustLevelTag.Value())
|
||||
if err = ValidateTrustLevel(string(trustLevel)); chk.E(err) {
|
||||
var trustLevelValue uint64
|
||||
if trustLevelValue, err = strconv.ParseUint(string(trustLevelTag.Value()), 10, 8); chk.E(err) {
|
||||
return nil, errorf.E("invalid trust level: %w", err)
|
||||
}
|
||||
trustLevel := TrustLevel(trustLevelValue)
|
||||
if err = ValidateTrustLevel(trustLevel); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -291,7 +296,7 @@ func (ta *TrustAct) Validate() (err error) {
|
||||
return errorf.E("target pubkey must be 64 hex characters")
|
||||
}
|
||||
|
||||
if err = ValidateTrustLevel(string(ta.TrustLevel)); chk.E(err) {
|
||||
if err = ValidateTrustLevel(ta.TrustLevel); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -342,6 +347,39 @@ func (ta *TrustAct) ShouldReplicate(kind uint16) bool {
|
||||
return ta.HasReplicationKind(kind)
|
||||
}
|
||||
|
||||
// ShouldReplicateEvent determines whether a specific event should be replicated
|
||||
// based on the trust level using partial replication (random dice-throw).
|
||||
// This function uses crypto/rand for cryptographically secure randomness.
|
||||
func (ta *TrustAct) ShouldReplicateEvent(kind uint16) (shouldReplicate bool, err error) {
|
||||
// Check if kind is eligible for replication
|
||||
if !ta.ShouldReplicate(kind) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Trust level of 100 means always replicate
|
||||
if ta.TrustLevel == TrustLevelFull {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Trust level of 0 means never replicate
|
||||
if ta.TrustLevel == TrustLevelNone {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Generate cryptographically secure random number 0-100
|
||||
var randomBytes [1]byte
|
||||
if _, err = rand.Read(randomBytes[:]); chk.E(err) {
|
||||
return false, errorf.E("failed to generate random number: %w", err)
|
||||
}
|
||||
|
||||
// Scale byte value (0-255) to 0-100 range
|
||||
randomValue := uint8((uint16(randomBytes[0]) * 101) / 256)
|
||||
|
||||
// Replicate if random value is less than or equal to trust level
|
||||
shouldReplicate = randomValue <= uint8(ta.TrustLevel)
|
||||
return
|
||||
}
|
||||
|
||||
// GetTargetPubkey returns the target relay's public key.
|
||||
func (ta *TrustAct) GetTargetPubkey() string {
|
||||
return ta.TargetPubkey
|
||||
|
||||
@@ -55,42 +55,65 @@ var (
|
||||
PublicKeyAdvertisementKind = kind.New(39103)
|
||||
DirectoryEventReplicationRequestKind = kind.New(39104)
|
||||
DirectoryEventReplicationResponseKind = kind.New(39105)
|
||||
GroupTagTransferKind = kind.New(39106)
|
||||
EscrowWitnessCompletionActKind = kind.New(39107)
|
||||
)
|
||||
|
||||
// Common tag names used across directory protocol messages
|
||||
var (
|
||||
DTag = []byte("d")
|
||||
RelayTag = []byte("relay")
|
||||
SigningKeyTag = []byte("signing_key")
|
||||
EncryptionKeyTag = []byte("encryption_key")
|
||||
VersionTag = []byte("version")
|
||||
NIP11URLTag = []byte("nip11_url")
|
||||
PubkeyTag = []byte("p")
|
||||
TrustLevelTag = []byte("trust_level")
|
||||
ExpiryTag = []byte("expiry")
|
||||
ReasonTag = []byte("reason")
|
||||
KTag = []byte("K")
|
||||
ITag = []byte("I")
|
||||
GroupTagTag = []byte("group_tag")
|
||||
ActorTag = []byte("actor")
|
||||
ConfidenceTag = []byte("confidence")
|
||||
PurposeTag = []byte("purpose")
|
||||
AlgorithmTag = []byte("algorithm")
|
||||
DerivationPathTag = []byte("derivation_path")
|
||||
KeyIndexTag = []byte("key_index")
|
||||
RequestIDTag = []byte("request_id")
|
||||
EventIDTag = []byte("event_id")
|
||||
StatusTag = []byte("status")
|
||||
ErrorTag = []byte("error")
|
||||
DTag = []byte("d")
|
||||
RelayTag = []byte("relay")
|
||||
SigningKeyTag = []byte("signing_key")
|
||||
EncryptionKeyTag = []byte("encryption_key")
|
||||
VersionTag = []byte("version")
|
||||
NIP11URLTag = []byte("nip11_url")
|
||||
PubkeyTag = []byte("p")
|
||||
TrustLevelTag = []byte("trust_level")
|
||||
ExpiryTag = []byte("expiry")
|
||||
ReasonTag = []byte("reason")
|
||||
KTag = []byte("K")
|
||||
ITag = []byte("I")
|
||||
GroupTagTag = []byte("group_tag")
|
||||
ActorTag = []byte("actor")
|
||||
ConfidenceTag = []byte("confidence")
|
||||
OwnersTag = []byte("owners")
|
||||
CreatedTag = []byte("created")
|
||||
FromOwnersTag = []byte("from_owners")
|
||||
ToOwnersTag = []byte("to_owners")
|
||||
TransferDateTag = []byte("transfer_date")
|
||||
SignaturesTag = []byte("signatures")
|
||||
EscrowIDTag = []byte("escrow_id")
|
||||
SellerWitnessTag = []byte("seller_witness")
|
||||
BuyerWitnessTag = []byte("buyer_witness")
|
||||
ConditionsTag = []byte("conditions")
|
||||
WitnessRoleTag = []byte("witness_role")
|
||||
CompletionStatusTag = []byte("completion_status")
|
||||
VerificationHashTag = []byte("verification_hash")
|
||||
TimestampTag = []byte("timestamp")
|
||||
PurposeTag = []byte("purpose")
|
||||
AlgorithmTag = []byte("algorithm")
|
||||
DerivationPathTag = []byte("derivation_path")
|
||||
KeyIndexTag = []byte("key_index")
|
||||
RequestIDTag = []byte("request_id")
|
||||
EventIDTag = []byte("event_id")
|
||||
StatusTag = []byte("status")
|
||||
ErrorTag = []byte("error")
|
||||
)
|
||||
|
||||
// Trust levels for trust acts
|
||||
type TrustLevel string
|
||||
// TrustLevel represents the replication percentage (0-100) indicating
|
||||
// the probability that any given event will be replicated.
|
||||
// This implements partial replication via random selection.
|
||||
type TrustLevel uint8
|
||||
|
||||
// Suggested trust level ranges
|
||||
const (
|
||||
TrustLevelHigh TrustLevel = "high"
|
||||
TrustLevelMedium TrustLevel = "medium"
|
||||
TrustLevelLow TrustLevel = "low"
|
||||
TrustLevelNone TrustLevel = 0 // No replication
|
||||
TrustLevelMinimal TrustLevel = 10 // Minimal sampling (10%)
|
||||
TrustLevelLow TrustLevel = 25 // Low partial replication (25%)
|
||||
TrustLevelMedium TrustLevel = 50 // Medium partial replication (50%)
|
||||
TrustLevelHigh TrustLevel = 75 // High partial replication (75%)
|
||||
TrustLevelFull TrustLevel = 100 // Full replication (100%)
|
||||
)
|
||||
|
||||
// Reason types for trust establishment
|
||||
@@ -163,14 +186,12 @@ func IsDirectoryEventKind(k uint16) (isDirectory bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateTrustLevel checks if the provided trust level is valid.
|
||||
func ValidateTrustLevel(level string) (err error) {
|
||||
switch TrustLevel(level) {
|
||||
case TrustLevelHigh, TrustLevelMedium, TrustLevelLow:
|
||||
return nil
|
||||
default:
|
||||
return errorf.E("invalid trust level: %s", level)
|
||||
// ValidateTrustLevel checks if the provided trust level is valid (0-100).
|
||||
func ValidateTrustLevel(level TrustLevel) (err error) {
|
||||
if level > 100 {
|
||||
return errorf.E("invalid trust level: %d (must be 0-100)", level)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateKeyPurpose checks if the provided key purpose is valid.
|
||||
|
||||
@@ -26,11 +26,34 @@ const (
|
||||
|
||||
// Regular expressions for validation
|
||||
var (
|
||||
hexKeyRegex = regexp.MustCompile(`^[0-9a-fA-F]{64}$`)
|
||||
npubRegex = regexp.MustCompile(`^npub1[0-9a-z]+$`)
|
||||
wsURLRegex = regexp.MustCompile(`^wss?://[a-zA-Z0-9.-]+(?::[0-9]+)?(?:/.*)?$`)
|
||||
hexKeyRegex = regexp.MustCompile(`^[0-9a-fA-F]{64}$`)
|
||||
npubRegex = regexp.MustCompile(`^npub1[0-9a-z]+$`)
|
||||
wsURLRegex = regexp.MustCompile(`^wss?://[a-zA-Z0-9.-]+(?::[0-9]+)?(?:/.*)?$`)
|
||||
groupTagNameRegex = regexp.MustCompile(`^[a-zA-Z0-9._~-]+$`) // RFC 3986 URL-safe characters
|
||||
)
|
||||
|
||||
// ValidateGroupTagName validates that a group tag name is URL-safe (RFC 3986).
|
||||
func ValidateGroupTagName(name string) (err error) {
|
||||
if len(name) < 1 {
|
||||
return errorf.E("group tag name cannot be empty")
|
||||
}
|
||||
if len(name) > 255 {
|
||||
return errorf.E("group tag name cannot exceed 255 characters")
|
||||
}
|
||||
|
||||
// Check for reserved prefixes
|
||||
if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
|
||||
return errorf.E("group tag names starting with '.' or '_' are reserved for system use")
|
||||
}
|
||||
|
||||
// Validate URL-safe character set
|
||||
if !groupTagNameRegex.MatchString(name) {
|
||||
return errorf.E("group tag name must contain only URL-safe characters (a-z, A-Z, 0-9, -, ., _, ~)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateHexKey validates that a string is a valid 64-character hex key.
|
||||
func ValidateHexKey(key string) (err error) {
|
||||
if !hexKeyRegex.MatchString(key) {
|
||||
|
||||
Reference in New Issue
Block a user