@@ -39,6 +39,27 @@ function formatError(message, options = {}) {
3939const DEFAULT_ICON_NAME = "default" ;
4040const DEFAULT_ICON_VIEWBOX = "0 0 24 24" ;
4141const SUPPORTED_PLUGIN_TYPES = [ "module" , "project" ] ;
42+ const RESERVED_NAMES = Object . freeze ( {
43+ routes : [
44+ "api" ,
45+ "auth" ,
46+ "login" ,
47+ "logout" ,
48+ "register" ,
49+ "ws" ,
50+ "public" ,
51+ "static" ,
52+ "assets" ,
53+ "css" ,
54+ "js" ,
55+ "manifest" ,
56+ "sw" ,
57+ "service-worker" ,
58+ "uploads" ,
59+ ] ,
60+ plugins : [ "sovereign" , "platform" , "core" , "admin" ] ,
61+ keywords : [ "favicon" , "robots" , "security" , "health" , "status" ] ,
62+ } ) ;
4263
4364function normalizeRoleValue ( role ) {
4465 if ( ! role ) return null ;
@@ -170,6 +191,7 @@ const manifest = {
170191 modules : [ ] ,
171192 enabledPlugins : [ ] , // [@<org>/<ns>]
172193 allowedPluginFrameworks : [ ] ,
194+ reservedNames : RESERVED_NAMES ,
173195 __rootdir,
174196 __pluginsdir,
175197 __datadir,
@@ -350,6 +372,10 @@ const exists = async (p) => {
350372
351373const buildManifest = async ( ) => {
352374 const plugins = { } ;
375+ const validationErrors = [ ] ;
376+ const seenNamespaces = new Set ( ) ;
377+ const seenIds = new Set ( ) ;
378+ const reserved = manifest . reservedNames || RESERVED_NAMES ;
353379
354380 // Preserve createdAt/instanceId and always update updatedAt
355381 let existingCreatedAt = null ;
@@ -476,6 +502,46 @@ const buildManifest = async () => {
476502
477503 // TODO: split `id` field by "/", and take id[1] as one of the fallback namespace value
478504 const manifestNamespace = pluginManifest . namespace || plugingDirName ;
505+ const manifestId = pluginManifest . id || "" ;
506+
507+ const isReserved =
508+ ( reserved ?. routes || [ ] ) . includes ( manifestNamespace ) ||
509+ ( reserved ?. plugins || [ ] ) . includes ( manifestNamespace ) ||
510+ ( reserved ?. keywords || [ ] ) . includes ( manifestNamespace ) ;
511+
512+ if ( isReserved ) {
513+ validationErrors . push (
514+ formatError (
515+ `✗ namespace "${ manifestNamespace } " is reserved (routes/plugins/keywords); update the plugin namespace.` ,
516+ { manifestPath : pluginManifestPath , pluginDir : plugingRoot }
517+ )
518+ ) ;
519+ continue ;
520+ }
521+
522+ if ( seenNamespaces . has ( manifestNamespace ) ) {
523+ validationErrors . push (
524+ formatError ( `✗ duplicate namespace "${ manifestNamespace } " detected` , {
525+ manifestPath : pluginManifestPath ,
526+ pluginDir : plugingRoot ,
527+ } )
528+ ) ;
529+ continue ;
530+ }
531+ seenNamespaces . add ( manifestNamespace ) ;
532+
533+ if ( manifestId ) {
534+ if ( seenIds . has ( manifestId ) ) {
535+ validationErrors . push (
536+ formatError ( `✗ duplicate plugin id "${ manifestId } " detected` , {
537+ manifestPath : pluginManifestPath ,
538+ pluginDir : plugingRoot ,
539+ } )
540+ ) ;
541+ continue ;
542+ }
543+ seenIds . add ( manifestId ) ;
544+ }
479545
480546 const enrollStrategy =
481547 pluginManifest . corePlugin === true
@@ -592,6 +658,11 @@ const buildManifest = async () => {
592658 } ;
593659 }
594660
661+ if ( validationErrors . length ) {
662+ validationErrors . forEach ( ( msg ) => console . error ( msg ) ) ;
663+ throw new Error ( `Manifest build failed: ${ validationErrors . length } validation error(s).` ) ;
664+ }
665+
595666 const finalPlugins = {
596667 ...manifest . plugins ,
597668 ...plugins ,
0 commit comments