@@ -12,6 +12,69 @@ function isPlainObject(value) {
1212 return ! ! value && typeof value === "object" && ! Array . isArray ( value ) ;
1313}
1414
15+ function isParentJsonLike ( value ) {
16+ if ( ! isPlainObject ( value ) ) {
17+ return false ;
18+ }
19+ const documentKind = typeof value . documentKind === "string" ? value . documentKind . trim ( ) . toLowerCase ( ) : "" ;
20+ const schema = typeof value . schema === "string" ? value . schema . trim ( ) . toLowerCase ( ) : "" ;
21+ if ( documentKind === "workspace-manifest" || schema === "html-js-gaming.project" || schema === "html-js-gaming.workspace" ) {
22+ return true ;
23+ }
24+ return isPlainObject ( value . tools ) ;
25+ }
26+
27+ function hasImplicitGlobalKey ( value ) {
28+ if ( ! isPlainObject ( value ) ) {
29+ return false ;
30+ }
31+ const blockedKeys = new Set ( [
32+ "launchparams" ,
33+ "sharedcontext" ,
34+ "hostcontextid" ,
35+ "hosttoolid" ,
36+ "hosted" ,
37+ "sampleid" ,
38+ "sampletitle" ,
39+ "samplepresetpath" ,
40+ "gameid" ,
41+ "gametitle" ,
42+ "gamehref" ,
43+ "workspacehref" ,
44+ "returnto" ,
45+ "state"
46+ ] ) ;
47+ return Object . keys ( value ) . some ( ( key ) => blockedKeys . has ( String ( key ) . trim ( ) . toLowerCase ( ) ) ) ;
48+ }
49+
50+ function assertExplicitLaunchInputs ( { toolId = "" , payloadJson = null , paletteJson = null , argumentCount = 0 } ) {
51+ const normalizedToolId = normalizeToolId ( toolId ) ;
52+ if ( ! normalizedToolId ) {
53+ throw new Error ( "launch contract violation: toolId is required." ) ;
54+ }
55+ if ( argumentCount < 2 || argumentCount > 3 ) {
56+ throw new Error ( `launch contract violation: launch(toolId, payloadJson, paletteJson?) expected 2-3 args, received ${ argumentCount } .` ) ;
57+ }
58+ if ( ! isPlainObject ( payloadJson ) ) {
59+ throw new Error ( `launch contract violation: payloadJson must be an object for ${ normalizedToolId } .` ) ;
60+ }
61+ if ( paletteJson !== null && ! isPlainObject ( paletteJson ) ) {
62+ throw new Error ( `launch contract violation: paletteJson must be an object or null for ${ normalizedToolId } .` ) ;
63+ }
64+ if ( isParentJsonLike ( payloadJson ) ) {
65+ throw new Error ( `launch contract violation: parent JSON usage detected in payloadJson for ${ normalizedToolId } .` ) ;
66+ }
67+ if ( paletteJson !== null && isParentJsonLike ( paletteJson ) ) {
68+ throw new Error ( `launch contract violation: parent JSON usage detected in paletteJson for ${ normalizedToolId } .` ) ;
69+ }
70+ if ( hasImplicitGlobalKey ( payloadJson ) ) {
71+ throw new Error ( `launch contract violation: implicit/global input keys detected in payloadJson for ${ normalizedToolId } .` ) ;
72+ }
73+ if ( paletteJson !== null && hasImplicitGlobalKey ( paletteJson ) ) {
74+ throw new Error ( `launch contract violation: implicit/global input keys detected in paletteJson for ${ normalizedToolId } .` ) ;
75+ }
76+ }
77+
1578function buildHostLaunchUrl ( toolEntry , hostContextId = "" ) {
1679 const url = new URL ( toolEntry . launchPath , window . location . href ) ;
1780 url . searchParams . set ( "hosted" , "1" ) ;
@@ -150,15 +213,13 @@ export function createToolHostRuntime(options = {}) {
150213 return null ;
151214 }
152215
216+ assertExplicitLaunchInputs ( {
217+ toolId,
218+ payloadJson,
219+ paletteJson,
220+ argumentCount : arguments . length
221+ } ) ;
153222 const normalizedToolId = normalizeToolId ( toolId ) ;
154- if ( ! isPlainObject ( payloadJson ) ) {
155- onStatus ( `Tool launch blocked: explicit payloadJson is required for ${ normalizedToolId || "(empty)" } .` ) ;
156- return null ;
157- }
158- if ( paletteJson !== null && ! isPlainObject ( paletteJson ) ) {
159- onStatus ( `Tool launch blocked: paletteJson must be an object when provided for ${ normalizedToolId || "(empty)" } .` ) ;
160- return null ;
161- }
162223 const toolEntry = getToolHostEntryById ( manifest , normalizedToolId ) ;
163224 if ( ! toolEntry ) {
164225 onStatus ( `Tool id not found: ${ normalizedToolId || "(empty)" } .` ) ;
0 commit comments