feat(plugin): gate pages on companion features

This commit is contained in:
npc0-hue
2026-07-29 09:53:39 +08:00
parent 271e1e684f
commit c9494abc48
16 changed files with 300 additions and 61 deletions
+35 -7
View File
@@ -66,6 +66,15 @@ type GameClientBridgePageContract struct {
CommandTypes []string
SnapshotTypes []string
QueryTemplateKeys []string
FeatureKeys []string
}
type GameClientBridgeFeatureDeclaration struct {
Key string
Title string
Permission string
RequiredHandlers []string
RequiredEventProducers []string
}
type GameClientBridgeCompanionDeclaration struct {
@@ -90,6 +99,7 @@ type GameClientBridgeManifest struct {
QueryTemplates []GameClientBridgeQueryTemplateDeclaration
Retention GameClientBridgeRetention
Pages []GameClientBridgePageContract
Features []GameClientBridgeFeatureDeclaration
Companion GameClientBridgeCompanionDeclaration
}
@@ -278,13 +288,21 @@ type GameClientBridgeSnapshotQuery struct {
}
type GameClientBridgeProfileDeclaration struct {
PluginID string
ProfileKey string
Available bool
Reason string
CommandTypes []string
SnapshotTypes []string
QueryTemplateKeys []string
PluginID string
ProfileKey string
Available bool
Reason string
CommandTypes []string
SnapshotTypes []string
QueryTemplateKeys []string
HandlerTypes []string
EventProducerTypes []string
}
type GameClientBridgeFeatureAvailability struct {
Key string
Available bool
Reason string
}
type GameClientBridgeStatus struct {
@@ -293,12 +311,15 @@ type GameClientBridgeStatus struct {
Available bool
Reason string
Profiles []GameClientBridgeProfileDeclaration
Features []GameClientBridgeFeatureAvailability
}
func CopyGameClientBridgeProfileDeclaration(value GameClientBridgeProfileDeclaration) GameClientBridgeProfileDeclaration {
value.CommandTypes = CopyStringSlice(value.CommandTypes)
value.SnapshotTypes = CopyStringSlice(value.SnapshotTypes)
value.QueryTemplateKeys = CopyStringSlice(value.QueryTemplateKeys)
value.HandlerTypes = CopyStringSlice(value.HandlerTypes)
value.EventProducerTypes = CopyStringSlice(value.EventProducerTypes)
return value
}
@@ -307,6 +328,7 @@ func CopyGameClientBridgeStatus(value GameClientBridgeStatus) GameClientBridgeSt
for index := range value.Profiles {
value.Profiles[index] = CopyGameClientBridgeProfileDeclaration(value.Profiles[index])
}
value.Features = append([]GameClientBridgeFeatureAvailability(nil), value.Features...)
return value
}
@@ -365,10 +387,16 @@ func CopyGameClientBridgeManifest(value GameClientBridgeManifest) GameClientBrid
value.Snapshots = append([]GameClientBridgeSnapshotDeclaration(nil), value.Snapshots...)
value.QueryTemplates = append([]GameClientBridgeQueryTemplateDeclaration(nil), value.QueryTemplates...)
value.Pages = append([]GameClientBridgePageContract(nil), value.Pages...)
value.Features = append([]GameClientBridgeFeatureDeclaration(nil), value.Features...)
for index := range value.Pages {
value.Pages[index].CommandTypes = CopyStringSlice(value.Pages[index].CommandTypes)
value.Pages[index].SnapshotTypes = CopyStringSlice(value.Pages[index].SnapshotTypes)
value.Pages[index].QueryTemplateKeys = CopyStringSlice(value.Pages[index].QueryTemplateKeys)
value.Pages[index].FeatureKeys = CopyStringSlice(value.Pages[index].FeatureKeys)
}
for index := range value.Features {
value.Features[index].RequiredHandlers = CopyStringSlice(value.Features[index].RequiredHandlers)
value.Features[index].RequiredEventProducers = CopyStringSlice(value.Features[index].RequiredEventProducers)
}
return value
}
+3 -1
View File
@@ -312,9 +312,10 @@ type GamePluginPage struct {
Key string
Title string
Path string
Bundle PluginPageBundle
Bundle PluginPageBundle
Permissions []string
BridgeActions []string
FeatureKeys []string
}
// PluginPageBundle identifies an installed plugin-owned page module. The host
@@ -1806,6 +1807,7 @@ func CopyGamePluginPageSlice(pages []GamePluginPage) []GamePluginPage {
out[i] = page
out[i].Permissions = CopyStringSlice(page.Permissions)
out[i].BridgeActions = CopyStringSlice(page.BridgeActions)
out[i].FeatureKeys = CopyStringSlice(page.FeatureKeys)
}
return out
}
+27 -14
View File
@@ -194,21 +194,30 @@ type GameClientBridgeAuditReferenceListResponse struct {
}
type GameClientBridgeProfileDeclarationResponse struct {
PluginID string `json:"pluginId"`
ProfileKey string `json:"profileKey"`
Available bool `json:"available"`
Reason string `json:"reason,omitempty"`
CommandTypes []string `json:"commandTypes"`
SnapshotTypes []string `json:"snapshotTypes"`
QueryTemplateKeys []string `json:"queryTemplateKeys"`
PluginID string `json:"pluginId"`
ProfileKey string `json:"profileKey"`
Available bool `json:"available"`
Reason string `json:"reason,omitempty"`
CommandTypes []string `json:"commandTypes"`
SnapshotTypes []string `json:"snapshotTypes"`
QueryTemplateKeys []string `json:"queryTemplateKeys"`
HandlerTypes []string `json:"handlerTypes"`
EventProducerTypes []string `json:"eventProducerTypes"`
}
type GameClientBridgeFeatureAvailabilityResponse struct {
Key string `json:"key"`
Available bool `json:"available"`
Reason string `json:"reason,omitempty"`
}
type GameClientBridgeStatusResponse struct {
ServerInstanceID string `json:"serverInstanceId"`
PluginID string `json:"pluginId"`
Available bool `json:"available"`
Reason string `json:"reason,omitempty"`
Profiles []GameClientBridgeProfileDeclarationResponse `json:"profiles"`
ServerInstanceID string `json:"serverInstanceId"`
PluginID string `json:"pluginId"`
Available bool `json:"available"`
Reason string `json:"reason,omitempty"`
Profiles []GameClientBridgeProfileDeclarationResponse `json:"profiles"`
Features []GameClientBridgeFeatureAvailabilityResponse `json:"features"`
}
func (request GameClientBridgeQueueRequest) ToDomain(serverID, pluginID string) domain.GameClientBridgeQueueRequest {
@@ -356,9 +365,13 @@ func GameClientBridgeStatusFromDomain(value domain.GameClientBridgeStatus) GameC
value = domain.CopyGameClientBridgeStatus(value)
profiles := make([]GameClientBridgeProfileDeclarationResponse, len(value.Profiles))
for index, profile := range value.Profiles {
profiles[index] = GameClientBridgeProfileDeclarationResponse{PluginID: profile.PluginID, ProfileKey: profile.ProfileKey, Available: profile.Available, Reason: profile.Reason, CommandTypes: nonNilStrings(profile.CommandTypes), SnapshotTypes: nonNilStrings(profile.SnapshotTypes), QueryTemplateKeys: nonNilStrings(profile.QueryTemplateKeys)}
profiles[index] = GameClientBridgeProfileDeclarationResponse{PluginID: profile.PluginID, ProfileKey: profile.ProfileKey, Available: profile.Available, Reason: profile.Reason, CommandTypes: nonNilStrings(profile.CommandTypes), SnapshotTypes: nonNilStrings(profile.SnapshotTypes), QueryTemplateKeys: nonNilStrings(profile.QueryTemplateKeys), HandlerTypes: nonNilStrings(profile.HandlerTypes), EventProducerTypes: nonNilStrings(profile.EventProducerTypes)}
}
return GameClientBridgeStatusResponse{ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, Available: value.Available, Reason: value.Reason, Profiles: profiles}
features := make([]GameClientBridgeFeatureAvailabilityResponse, len(value.Features))
for index, feature := range value.Features {
features[index] = GameClientBridgeFeatureAvailabilityResponse{Key: feature.Key, Available: feature.Available, Reason: feature.Reason}
}
return GameClientBridgeStatusResponse{ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, Available: value.Available, Reason: value.Reason, Profiles: profiles, Features: features}
}
func gameClientBridgeCommandResultFromDomain(value domain.GameClientBridgeResult) GameClientBridgeCommandResultResponse {
+40 -19
View File
@@ -176,14 +176,15 @@ type PluginLifecycleActionsBody struct {
}
type GamePluginPageBody struct {
Key string `json:"key"`
Title string `json:"title"`
Path string `json:"path"`
BundleKey string `json:"bundleKey"`
BundleVersion string `json:"bundleVersion"`
BundleIntegritySHA256 string `json:"bundleIntegritySha256"`
Permissions []string `json:"permissions,omitempty"`
BridgeActions []string `json:"bridgeActions,omitempty"`
Key string `json:"key"`
Title string `json:"title"`
Path string `json:"path"`
BundleKey string `json:"bundleKey"`
BundleVersion string `json:"bundleVersion"`
BundleIntegritySHA256 string `json:"bundleIntegritySha256"`
Permissions []string `json:"permissions,omitempty"`
BridgeActions []string `json:"bridgeActions,omitempty"`
FeatureKeys []string `json:"featureKeys,omitempty"`
}
type PluginLogicalDirectoryBody struct {
@@ -297,6 +298,15 @@ type GameClientBridgePageContractBody struct {
CommandTypes []string `json:"commandTypes,omitempty"`
SnapshotTypes []string `json:"snapshotTypes,omitempty"`
QueryTemplateKeys []string `json:"queryTemplateKeys,omitempty"`
FeatureKeys []string `json:"featureKeys,omitempty"`
}
type GameClientBridgeFeatureDeclarationBody struct {
Key string `json:"key"`
Title string `json:"title"`
Permission string `json:"permission"`
RequiredHandlers []string `json:"requiredHandlers,omitempty"`
RequiredEventProducers []string `json:"requiredEventProducers,omitempty"`
}
type GameClientBridgeCompanionDeclarationBody struct {
@@ -322,6 +332,7 @@ type GameClientBridgeManifestBody struct {
CommandRetentionSeconds int `json:"commandRetentionSeconds"`
MaxCommands int `json:"maxCommands"`
Pages []GameClientBridgePageContractBody `json:"pages,omitempty"`
Features []GameClientBridgeFeatureDeclarationBody `json:"features,omitempty"`
Companion *GameClientBridgeCompanionDeclarationBody `json:"companion,omitempty"`
}
type GameMapTrajectoryDeclarationBody struct {
@@ -1111,13 +1122,17 @@ func (body GameClientBridgeManifestBody) ToDomain() domain.GameClientBridgeManif
}
pages := make([]domain.GameClientBridgePageContract, len(body.Pages))
for index, page := range body.Pages {
pages[index] = domain.GameClientBridgePageContract{PageKey: page.PageKey, CommandTypes: domain.CopyStringSlice(page.CommandTypes), SnapshotTypes: domain.CopyStringSlice(page.SnapshotTypes), QueryTemplateKeys: domain.CopyStringSlice(page.QueryTemplateKeys)}
pages[index] = domain.GameClientBridgePageContract{PageKey: page.PageKey, CommandTypes: domain.CopyStringSlice(page.CommandTypes), SnapshotTypes: domain.CopyStringSlice(page.SnapshotTypes), QueryTemplateKeys: domain.CopyStringSlice(page.QueryTemplateKeys), FeatureKeys: domain.CopyStringSlice(page.FeatureKeys)}
}
features := make([]domain.GameClientBridgeFeatureDeclaration, len(body.Features))
for index, feature := range body.Features {
features[index] = domain.GameClientBridgeFeatureDeclaration{Key: feature.Key, Title: feature.Title, Permission: feature.Permission, RequiredHandlers: domain.CopyStringSlice(feature.RequiredHandlers), RequiredEventProducers: domain.CopyStringSlice(feature.RequiredEventProducers)}
}
companion := domain.GameClientBridgeCompanionDeclaration{}
if body.Companion != nil {
companion = domain.GameClientBridgeCompanionDeclaration{ProfileKey: body.Companion.ProfileKey, ConfigTemplateKey: body.Companion.ConfigTemplateKey, ConfigSchemaRef: body.Companion.ConfigSchemaRef, ConfigFormat: body.Companion.ConfigFormat, PlatformBaseURLSource: body.Companion.PlatformBaseURLSource, RegistrationProof: body.Companion.RegistrationProof, ProofMaterialSource: body.Companion.ProofMaterialSource, ProofMaterialEnv: body.Companion.ProofMaterialEnv, SessionMode: body.Companion.SessionMode, TLSPolicy: body.Companion.TLSPolicy, HeartbeatIntervalSeconds: body.Companion.HeartbeatIntervalSeconds, CommandPollIntervalSeconds: body.Companion.CommandPollIntervalSeconds, RequestTimeoutSeconds: body.Companion.RequestTimeoutSeconds}
}
return domain.GameClientBridgeManifest{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, Retention: domain.GameClientBridgeRetention{KeepForSeconds: body.CommandRetentionSeconds, MaxRecords: body.MaxCommands}, Pages: pages, Companion: companion}
return domain.GameClientBridgeManifest{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, Retention: domain.GameClientBridgeRetention{KeepForSeconds: body.CommandRetentionSeconds, MaxRecords: body.MaxCommands}, Pages: pages, Features: features, Companion: companion}
}
func (actions PluginLifecycleActionsBody) ToDomain() domain.PluginLifecycleActions {
@@ -1524,13 +1539,17 @@ func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) G
}
pages := make([]GameClientBridgePageContractBody, len(value.Pages))
for index, page := range value.Pages {
pages[index] = GameClientBridgePageContractBody{PageKey: page.PageKey, CommandTypes: page.CommandTypes, SnapshotTypes: page.SnapshotTypes, QueryTemplateKeys: page.QueryTemplateKeys}
pages[index] = GameClientBridgePageContractBody{PageKey: page.PageKey, CommandTypes: page.CommandTypes, SnapshotTypes: page.SnapshotTypes, QueryTemplateKeys: page.QueryTemplateKeys, FeatureKeys: page.FeatureKeys}
}
features := make([]GameClientBridgeFeatureDeclarationBody, len(value.Features))
for index, feature := range value.Features {
features[index] = GameClientBridgeFeatureDeclarationBody{Key: feature.Key, Title: feature.Title, Permission: feature.Permission, RequiredHandlers: feature.RequiredHandlers, RequiredEventProducers: feature.RequiredEventProducers}
}
var companion *GameClientBridgeCompanionDeclarationBody
if value.Companion.ProfileKey != "" {
companion = &GameClientBridgeCompanionDeclarationBody{ProfileKey: value.Companion.ProfileKey, ConfigTemplateKey: value.Companion.ConfigTemplateKey, ConfigSchemaRef: value.Companion.ConfigSchemaRef, ConfigFormat: value.Companion.ConfigFormat, PlatformBaseURLSource: value.Companion.PlatformBaseURLSource, RegistrationProof: value.Companion.RegistrationProof, ProofMaterialSource: value.Companion.ProofMaterialSource, ProofMaterialEnv: value.Companion.ProofMaterialEnv, SessionMode: value.Companion.SessionMode, TLSPolicy: value.Companion.TLSPolicy, HeartbeatIntervalSeconds: value.Companion.HeartbeatIntervalSeconds, CommandPollIntervalSeconds: value.Companion.CommandPollIntervalSeconds, RequestTimeoutSeconds: value.Companion.RequestTimeoutSeconds}
}
return GameClientBridgeManifestBody{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, CommandRetentionSeconds: value.Retention.KeepForSeconds, MaxCommands: value.Retention.MaxRecords, Pages: pages, Companion: companion}
return GameClientBridgeManifestBody{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, CommandRetentionSeconds: value.Retention.KeepForSeconds, MaxCommands: value.Retention.MaxRecords, Pages: pages, Features: features, Companion: companion}
}
func MarketplacePluginListFromDomain(plugins []domain.PluginMarketplacePlugin) MarketplacePluginListResponse {
@@ -1886,6 +1905,7 @@ func pagesToDomain(pages []GamePluginPageBody) []domain.GamePluginPage {
Bundle: domain.PluginPageBundle{Key: page.BundleKey, Version: page.BundleVersion, IntegritySHA256: page.BundleIntegritySHA256},
Permissions: domain.CopyStringSlice(page.Permissions),
BridgeActions: domain.CopyStringSlice(page.BridgeActions),
FeatureKeys: domain.CopyStringSlice(page.FeatureKeys),
}
}
return out
@@ -1898,14 +1918,15 @@ func pagesFromDomain(pages []domain.GamePluginPage) []GamePluginPageBody {
out := make([]GamePluginPageBody, len(pages))
for i, page := range pages {
out[i] = GamePluginPageBody{
Key: page.Key,
Title: page.Title,
Path: page.Path,
BundleKey: page.Bundle.Key,
BundleVersion: page.Bundle.Version,
Key: page.Key,
Title: page.Title,
Path: page.Path,
BundleKey: page.Bundle.Key,
BundleVersion: page.Bundle.Version,
BundleIntegritySHA256: page.Bundle.IntegritySHA256,
Permissions: domain.CopyStringSlice(page.Permissions),
BridgeActions: domain.CopyStringSlice(page.BridgeActions),
Permissions: domain.CopyStringSlice(page.Permissions),
BridgeActions: domain.CopyStringSlice(page.BridgeActions),
FeatureKeys: domain.CopyStringSlice(page.FeatureKeys),
}
}
return out
+51 -1
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"reflect"
"sort"
"strings"
"time"
"browser.local/platform/domain"
@@ -55,7 +56,7 @@ func (svc *CoreService) GetGameClientBridgeStatusForSession(sessionID, serverIns
if err != nil {
return domain.GameClientBridgeStatus{}, err
}
status := domain.GameClientBridgeStatus{ServerInstanceID: instance.ID, PluginID: plugin.ID, Reason: "plugin does not declare a game client bridge profile", Profiles: []domain.GameClientBridgeProfileDeclaration{}}
status := domain.GameClientBridgeStatus{ServerInstanceID: instance.ID, PluginID: plugin.ID, Reason: "plugin does not declare a game client bridge profile", Profiles: []domain.GameClientBridgeProfileDeclaration{}, Features: []domain.GameClientBridgeFeatureAvailability{}}
installations, err := svc.store.ClientManagerInstallations().List(domain.ClientManagerInstallationFilter{ServerInstanceID: instance.ID})
if err != nil {
return domain.GameClientBridgeStatus{}, err
@@ -77,6 +78,8 @@ func (svc *CoreService) GetGameClientBridgeStatusForSession(sessionID, serverIns
if svc.now().Before(session.ExpiresAt) && containsString(session.Capabilities, gameClientBridgeCapability) && session.KeyGeneration == installation.KeyGeneration && session.DeploymentGeneration == installation.DeploymentGeneration && session.ArtifactID == installation.ActiveArtifactID {
declaration.Available = true
declaration.Reason = ""
declaration.HandlerTypes = append(declaration.HandlerTypes, gameClientBridgeSessionCapabilityValues(session.Capabilities, "handler.")...)
declaration.EventProducerTypes = append(declaration.EventProducerTypes, gameClientBridgeSessionCapabilityValues(session.Capabilities, "event-producer.")...)
break
}
}
@@ -90,9 +93,56 @@ func (svc *CoreService) GetGameClientBridgeStatusForSession(sessionID, serverIns
if status.Available {
status.Reason = ""
}
status.Features = gameClientBridgeFeatureAvailability(plugin.GameClientBridge.Features, status.Profiles)
return domain.CopyGameClientBridgeStatus(status), nil
}
func gameClientBridgeSessionCapabilityValues(capabilities []string, prefix string) []string {
values := make([]string, 0, len(capabilities))
for _, capability := range capabilities {
if value, found := strings.CutPrefix(capability, prefix); found && value != "" {
values = append(values, value)
}
}
sort.Strings(values)
return values
}
func gameClientBridgeFeatureAvailability(features []domain.GameClientBridgeFeatureDeclaration, profiles []domain.GameClientBridgeProfileDeclaration) []domain.GameClientBridgeFeatureAvailability {
result := make([]domain.GameClientBridgeFeatureAvailability, 0, len(features))
for _, feature := range features {
handlers, producers := map[string]bool{}, map[string]bool{}
for _, profile := range profiles {
if !profile.Available {
continue
}
for _, handler := range profile.HandlerTypes {
handlers[handler] = true
}
for _, producer := range profile.EventProducerTypes {
producers[producer] = true
}
}
missing := make([]string, 0)
for _, handler := range feature.RequiredHandlers {
if !handlers[handler] {
missing = append(missing, "handler."+handler)
}
}
for _, producer := range feature.RequiredEventProducers {
if !producers[producer] {
missing = append(missing, "event-producer."+producer)
}
}
availability := domain.GameClientBridgeFeatureAvailability{Key: feature.Key, Available: len(missing) == 0}
if len(missing) > 0 {
availability.Reason = "compatible Companion is missing " + strings.Join(missing, ", ")
}
result = append(result, availability)
}
return result
}
func (svc *CoreService) ListGameClientBridgeCommandsForSession(sessionID string, filter domain.GameClientBridgeCommandFilter) ([]domain.GameClientBridgeCommand, error) {
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
return nil, err
+80 -6
View File
@@ -361,7 +361,7 @@ func ValidatePluginCreateInputs(fields []domain.PluginCreateField, inputs map[st
func validateGameClientBridgeManifest(field string, bridge domain.GameClientBridgeManifest, permissions []string, pages []domain.GamePluginPage, runtimeProfiles domain.GamePluginRuntimeProfiles) []string {
companionPresent := bridge.Companion != (domain.GameClientBridgeCompanionDeclaration{})
if len(bridge.Commands) == 0 && len(bridge.Snapshots) == 0 && len(bridge.QueryTemplates) == 0 && len(bridge.Pages) == 0 && bridge.Retention.KeepForSeconds == 0 && bridge.Retention.MaxRecords == 0 && !companionPresent {
if len(bridge.Commands) == 0 && len(bridge.Snapshots) == 0 && len(bridge.QueryTemplates) == 0 && len(bridge.Pages) == 0 && len(bridge.Features) == 0 && bridge.Retention.KeepForSeconds == 0 && bridge.Retention.MaxRecords == 0 && !companionPresent {
return nil
}
var violations []string
@@ -515,6 +515,50 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
for _, page := range pages {
pageDeclarations[page.Key] = page
}
features := map[string]domain.GameClientBridgeFeatureDeclaration{}
for index, feature := range bridge.Features {
prefix := fmt.Sprintf("%s.features[%d]", field, index)
if !clientManagerIdentifierPattern.MatchString(feature.Key) || unsafeGameClientBridgeCommandType(feature.Key) {
violations = append(violations, prefix+".key is invalid or unsafe")
}
if _, exists := features[feature.Key]; exists {
violations = append(violations, prefix+".key is duplicated")
}
features[feature.Key] = feature
if strings.TrimSpace(feature.Title) == "" || len([]rune(feature.Title)) > 80 {
violations = append(violations, prefix+".title is invalid")
}
if !containsString(permissions, feature.Permission) {
violations = append(violations, prefix+".permission must be declared by the plugin")
}
if len(feature.RequiredHandlers) == 0 && len(feature.RequiredEventProducers) == 0 {
violations = append(violations, prefix+" must require a handler or event producer")
}
for _, handler := range feature.RequiredHandlers {
if !clientManagerIdentifierPattern.MatchString(handler) {
violations = append(violations, prefix+".requiredHandlers contains an invalid handler")
}
}
for _, producer := range feature.RequiredEventProducers {
if !clientManagerIdentifierPattern.MatchString(producer) {
violations = append(violations, prefix+".requiredEventProducers contains an invalid producer")
}
}
violations = append(violations, duplicateViolations(prefix+".requiredHandlers", feature.RequiredHandlers)...)
violations = append(violations, duplicateViolations(prefix+".requiredEventProducers", feature.RequiredEventProducers)...)
}
for _, page := range pages {
for _, featureKey := range page.FeatureKeys {
feature, exists := features[featureKey]
if !exists {
violations = append(violations, field+" page "+page.Key+" references undeclared feature "+featureKey)
continue
}
if !containsString(page.Permissions, feature.Permission) {
violations = append(violations, field+" page "+page.Key+" must declare feature permission "+feature.Permission)
}
}
}
for index, page := range bridge.Pages {
prefix := fmt.Sprintf("%s.pages[%d]", field, index)
pageDeclaration, pageExists := pageDeclarations[page.PageKey]
@@ -551,6 +595,16 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
violations = append(violations, prefix+" must declare remote.access.request for query templates")
}
}
for _, featureKey := range page.FeatureKeys {
feature, exists := features[featureKey]
if !exists {
violations = append(violations, prefix+" references undeclared feature "+featureKey)
continue
}
if !containsString(pageDeclaration.Permissions, feature.Permission) {
violations = append(violations, prefix+" must declare feature permission "+feature.Permission)
}
}
}
return violations
}
@@ -1401,7 +1455,9 @@ func validatePluginPages(pages []domain.GamePluginPage) []string {
violations = appendRequired(violations, prefix+".bundle.key", page.Bundle.Key)
violations = appendRequired(violations, prefix+".bundle.version", page.Bundle.Version)
violations = appendRequired(violations, prefix+".bundle.integritySha256", page.Bundle.IntegritySHA256)
if !validDistributionLogicalKey(page.Bundle.Key) || !validPluginPageBundleVersion(page.Bundle.Version) || !validPluginPageBundleIntegrity(page.Bundle.IntegritySHA256) { violations = append(violations, prefix+".bundle is invalid") }
if !validDistributionLogicalKey(page.Bundle.Key) || !validPluginPageBundleVersion(page.Bundle.Version) || !validPluginPageBundleIntegrity(page.Bundle.IntegritySHA256) {
violations = append(violations, prefix+".bundle is invalid")
}
}
if page.Key != "" {
if _, exists := seenKeys[page.Key]; exists {
@@ -1416,19 +1472,37 @@ func validatePluginPages(pages []domain.GamePluginPage) []string {
}
violations = append(violations, duplicateViolations(prefix+".permissions", page.Permissions)...)
violations = append(violations, validateBridgeActions(prefix+".bridgeActions", page.BridgeActions)...)
for _, key := range page.FeatureKeys {
if !clientManagerIdentifierPattern.MatchString(key) {
violations = append(violations, prefix+".featureKeys contains an invalid feature key")
}
}
violations = append(violations, duplicateViolations(prefix+".featureKeys", page.FeatureKeys)...)
}
return violations
}
func validPluginPageBundleVersion(value string) bool {
if len(value) == 0 || len(value) > 80 { return false }
for _, item := range value { if !(item >= 'a' && item <= 'z' || item >= 'A' && item <= 'Z' || item >= '0' && item <= '9' || item == '.' || item == '_' || item == '-') { return false } }
if len(value) == 0 || len(value) > 80 {
return false
}
for _, item := range value {
if !(item >= 'a' && item <= 'z' || item >= 'A' && item <= 'Z' || item >= '0' && item <= '9' || item == '.' || item == '_' || item == '-') {
return false
}
}
return true
}
func validPluginPageBundleIntegrity(value string) bool {
if len(value) != len("sha256:")+64 || !strings.HasPrefix(value, "sha256:") { return false }
for _, item := range value[len("sha256:"):] { if !(item >= 'a' && item <= 'f' || item >= '0' && item <= '9') { return false } }
if len(value) != len("sha256:")+64 || !strings.HasPrefix(value, "sha256:") {
return false
}
for _, item := range value[len("sha256:"):] {
if !(item >= 'a' && item <= 'f' || item >= '0' && item <= '9') {
return false
}
}
return true
}