feat: harden support, localize knowledge and add operator availability [skip ci]

Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.

Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.

Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.

Expected source tree: 0a4a7528e9.
This commit is contained in:
2026-10-16 15:21:42 +00:00
parent 299ea50fb5
commit 2f7a491666
176 changed files with 11869 additions and 2716 deletions

View File

@@ -1,6 +1,10 @@
// Modified in the independent hardening fork; see docs/hardening.
package services
import (
"crypto/rand"
"encoding/hex"
"errors"
"strings"
"time"
@@ -36,16 +40,19 @@ type customerSessionService struct {
}
type customerSessionClaims struct {
TokenType string `json:"typ"`
ChannelID int64 `json:"channelId"`
ChannelCode string `json:"channelCode"`
CustomerID int64 `json:"customerId"`
CustomerName string `json:"customerName"`
IdentityKey string `json:"identityKey"`
TokenType string `json:"typ"`
ChannelID int64 `json:"channelId"`
ChannelCode string `json:"channelCode"`
CustomerID int64 `json:"customerId"`
CustomerName string `json:"customerName"`
IdentityKey string `json:"identityKey"`
IdentityScope string `json:"identityScope"`
jwt.RegisteredClaims
}
type CustomerSessionVerifyResult struct {
CustomerID int64
ChannelID int64
ExternalUser *openidentity.ExternalUser
Token string
ExpiresAt time.Time
@@ -56,6 +63,19 @@ func (s *customerSessionService) Exchange(channel *models.Channel, externalUser
if channel == nil || channel.Status != enums.StatusOk {
return nil, errorsx.InvalidParamI18n("error.e0209")
}
if !openidentity.ValidIdentityScope(channel.ChannelID, externalUser) || (ChannelService.RequiresSignedIdentity(channel) && externalUser.ExternalSource != enums.ExternalSourceUser) {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
externalUser.ChannelID = channel.ID
if externalUser.ExternalSource == enums.ExternalSourceGuest {
// A public caller's guest ID is not proof of possession. Only the signed
// customer session can resume this server-issued anonymous identity.
var id [32]byte
if _, err := rand.Read(id[:]); err != nil {
return nil, err
}
externalUser.ExternalID = hex.EncodeToString(id[:])
}
var customerID int64
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
id, err := CustomerService.EnsureExternalCustomer(ctx, externalUser)
@@ -68,9 +88,13 @@ func (s *customerSessionService) Exchange(channel *models.Channel, externalUser
return nil, err
}
customer := CustomerService.Get(customerID)
if customer == nil || customer.Status == enums.StatusDeleted {
if customer == nil || customer.Status != enums.StatusOk {
return nil, errorsx.InvalidParamI18n("error.e0155")
}
identity, identityErr := repositories.CustomerIdentityRepository.GetUniqueBy(sqls.DB(), externalUser.IdentityScope, externalUser.ExternalSource, externalUser.ExternalID)
if identityErr != nil || identity == nil || identity.CustomerID != customer.ID {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
token, expiresAt, err := s.Sign(channel, customer, externalUser)
if err != nil {
return nil, err
@@ -92,18 +116,19 @@ func (s *customerSessionService) Sign(channel *models.Channel, customer *models.
if secret == "" {
return "", time.Time{}, errorsx.BusinessErrorI18n(1, "error.customerSession.secretMissing")
}
if channel == nil || customer == nil {
if channel == nil || customer == nil || !openidentity.ValidIdentityScope(channel.ChannelID, externalUser) {
return "", time.Time{}, errorsx.InvalidParamI18n("error.e0158")
}
now := time.Now()
expiresAt := now.Add(time.Duration(cfg.TTL()) * time.Minute)
claims := customerSessionClaims{
TokenType: customerSessionTokenType,
ChannelID: channel.ID,
ChannelCode: strings.TrimSpace(channel.ChannelID),
CustomerID: customer.ID,
CustomerName: strings.TrimSpace(customer.Name),
IdentityKey: s.identityKey(externalUser),
TokenType: customerSessionTokenType,
ChannelID: channel.ID,
ChannelCode: strings.TrimSpace(channel.ChannelID),
CustomerID: customer.ID,
CustomerName: strings.TrimSpace(customer.Name),
IdentityKey: s.identityKey(externalUser),
IdentityScope: externalUser.IdentityScope,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(expiresAt),
@@ -125,27 +150,13 @@ func (s *customerSessionService) VerifyRequest(ctx *gin.Context, channel *models
if err != nil {
return nil, err
}
if channel == nil || channel.Status != enums.StatusOk {
return nil, errorsx.InvalidParamI18n("error.e0209")
}
if claims.ChannelID != channel.ID || strings.TrimSpace(claims.ChannelCode) != strings.TrimSpace(channel.ChannelID) {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
customer := CustomerService.Get(claims.CustomerID)
if customer == nil || customer.Status == enums.StatusDeleted {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
external, err := s.externalUserFromClaims(claims, customer)
result, err := s.verifyClaims(claims, channel)
if err != nil {
return nil, err
}
result := &CustomerSessionVerifyResult{
ExternalUser: external,
Token: token,
ExpiresAt: claims.ExpiresAt.Time,
}
result.Token = token
if s.shouldRefresh(claims.ExpiresAt.Time) {
newToken, expiresAt, err := s.Sign(channel, customer, *external)
newToken, expiresAt, err := s.Sign(channel, CustomerService.Get(result.CustomerID), *result.ExternalUser)
if err != nil {
return nil, err
}
@@ -190,7 +201,7 @@ func (s *customerSessionService) verifyToken(rawToken string) (*customerSessionC
if token == nil || !token.Valid || claims.TokenType != customerSessionTokenType || claims.ExpiresAt == nil {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
if claims.ChannelID <= 0 || strings.TrimSpace(claims.ChannelCode) == "" || claims.CustomerID <= 0 || strings.TrimSpace(claims.IdentityKey) == "" {
if claims.ChannelID <= 0 || strings.TrimSpace(claims.ChannelCode) == "" || claims.CustomerID <= 0 || strings.TrimSpace(claims.IdentityKey) == "" || strings.TrimSpace(claims.IdentityScope) == "" {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
return claims, nil
@@ -211,8 +222,8 @@ func (s *customerSessionService) externalUserFromClaims(claims *customerSessionC
default:
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
identity := repositories.CustomerIdentityRepository.GetBy(sqls.DB(), source, parts[1])
if identity == nil || identity.CustomerID != claims.CustomerID {
identity, err := repositories.CustomerIdentityRepository.GetUniqueBy(sqls.DB(), claims.IdentityScope, source, parts[1])
if err != nil || identity == nil || identity.CustomerID != claims.CustomerID {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
name := strings.TrimSpace(claims.CustomerName)
@@ -223,6 +234,8 @@ func (s *customerSessionService) externalUserFromClaims(claims *customerSessionC
ExternalSource: source,
ExternalID: parts[1],
ExternalName: name,
IdentityScope: claims.IdentityScope,
ChannelID: claims.ChannelID,
}, nil
}
@@ -250,3 +263,38 @@ func (s *customerSessionService) getCustomerSessionToken(ctx *gin.Context) strin
token, _ := params.Get(ctx, "customerSessionToken")
return strings.TrimSpace(token)
}
// VerifyConnection rechecks the signed credential, expiry, channel policy and
// current identity mapping. It never refreshes an idle connection indefinitely.
func (s *customerSessionService) VerifyConnection(result *CustomerSessionVerifyResult) bool {
if result == nil || !time.Now().Before(result.ExpiresAt) {
return false
}
claims, err := s.verifyToken(result.Token)
if err != nil || claims.CustomerID != result.CustomerID || claims.ChannelID != result.ChannelID {
return false
}
verified, err := s.verifyClaims(claims, ChannelService.Get(claims.ChannelID))
return err == nil && verified.ExternalUser != nil && result.ExternalUser != nil &&
verified.ExternalUser.IdentityScope == result.ExternalUser.IdentityScope &&
verified.ExternalUser.ExternalSource == result.ExternalUser.ExternalSource &&
verified.ExternalUser.ExternalID == result.ExternalUser.ExternalID
}
func (s *customerSessionService) verifyClaims(claims *customerSessionClaims, channel *models.Channel) (*CustomerSessionVerifyResult, error) {
if channel == nil || channel.Status != enums.StatusOk || claims.ChannelID != channel.ID || strings.TrimSpace(claims.ChannelCode) != strings.TrimSpace(channel.ChannelID) {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
customer := CustomerService.Get(claims.CustomerID)
if customer == nil || customer.Status != enums.StatusOk {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
external, err := s.externalUserFromClaims(claims, customer)
if err != nil {
return nil, err
}
if !openidentity.ValidIdentityScope(channel.ChannelID, *external) || (ChannelService.RequiresSignedIdentity(channel) && external.ExternalSource != enums.ExternalSourceUser) {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
return &CustomerSessionVerifyResult{CustomerID: customer.ID, ChannelID: channel.ID, ExternalUser: external, ExpiresAt: claims.ExpiresAt.Time}, nil
}