char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
688e96e
main
1# [ path = "common/http.rs" ] 2mod http_fixture; 3# [ path = "common/registry.rs" ] 4mod registry_fixture; 5 6use std:: sync:: Arc ; 7 8use avril_core:: ModuleLoader ; 9use avril_runtime:: authority:: SourceAuthority ; 10use avril_runtime:: caps::{ Capabilities , Category , Initial , PathTarget }; 11use avril_runtime:: modules:: ModuleGraph ; 12use avril_runtime:: registry:: PackageStore ; 13use futures:: executor:: block_on; 14use http_fixture:: Http ; 15use registry_fixture:: Registry ; 16 17fn caps_with ( cwd : & std:: path:: Path , grants : & [( & str , bool , & str )]) ->Arc < Capabilities > { 18let mut initial =Initial :: new ( true ); 19for ( category, deny, value) in grants{ 20let category: Category = category. parse (). unwrap (); 21 initial. grant ( category, * deny, value, cwd). unwrap (); 22} 23Capabilities :: new ( initial, cwd. to_owned ()) 24} 25 26fn net_host_port ( url : & url:: Url ) ->String { 27format! ( 28"{}:{}" , 29 url. host_str (). unwrap (), 30 url. port_or_known_default (). unwrap () 31) 32} 33 34fn store_with ( 35options : avril_runtime:: registry:: Options , 36authority : SourceAuthority , 37) ->PackageStore { 38PackageStore :: new_with_authority ( registry_fixture:: runtime_handle (), options, authority) 39. unwrap () 40} 41 42fn graph_options ( dir : & std:: path:: Path ) -> avril_runtime:: registry:: Options { 43 avril_runtime:: registry:: Options { 44jsr_registry : url:: Url :: parse ( "https://example.invalid/" ). unwrap (), 45npm_registry : url:: Url :: parse ( "https://example.invalid/" ). unwrap (), 46cache : dir. join ( "cache" ), 47lock : dir. join ( "avril.lock" ), 48project_root : dir. to_owned (), 49offline : false , 50frozen : false , 51update : false , 52ephemeral_lock : false , 53vendor : None , 54} 55} 56 57# [ test ] 58fn execute_keeps_redirect_without_authorizing_target () { 59let http =Http :: new (); 60let target =Http :: new (); 61 http. redirect ( "start" , & target. name ( "nested/mod.ts" )); 62 target. source ( "nested/mod.ts" , "export const value = 7;" ); 63let name = http. name ( "start" ); 64 65let dir = tempfile:: tempdir (). unwrap (); 66let server =Registry :: new (); 67let caps =caps_with ( 68 dir. path (), 69& [ 70( "js-exec" , false , & name), 71( "js-exec" , true , target. url . as_str ()), 72], 73); 74let store =store_with ( server. options ( dir. path ()), SourceAuthority :: execute ( caps)); 75assert_eq! ( 76block_on ( store. remote_source ( & name)). unwrap (), 77"export const value = 7;" 78); 79} 80 81# [ test ] 82fn read_checks_network_for_redirect_targets () { 83let http =Http :: new (); 84let target =Http :: new (); 85 http. redirect ( "start" , & target. name ( "nested/mod.ts" )); 86 target. source ( "nested/mod.ts" , "export const value = 7;" ); 87let name = http. name ( "start" ); 88let http_url = url:: Url :: parse ( & name). unwrap (); 89 90// Net for the requested host only: redirect target denies the read. 91let dir = tempfile:: tempdir (). unwrap (); 92let server =Registry :: new (); 93let lock = dir. path (). join ( "avril.lock" ). to_string_lossy (). into_owned (); 94let only_first =caps_with ( 95 dir. path (), 96& [ 97( "net" , false , & net_host_port ( & http_url)), 98( "read" , false , & lock), 99], 100); 101let store =store_with ( 102 server. options ( dir. path ()), 103SourceAuthority :: read ( only_first), 104); 105assert! ( 106block_on ( store. remote_source ( & name)) 107. unwrap_err () 108. to_string () 109. contains ( "PermissionDenied" ) 110); 111 112assert! ( target. requests . lock (). unwrap (). is_empty ()); 113 114// Net for both hosts: the read succeeds. 115let dir2 = tempfile:: tempdir (). unwrap (); 116let server2 =Registry :: new (); 117let lock2 = dir2 118. path () 119. join ( "avril.lock" ) 120. to_string_lossy () 121. into_owned (); 122let both =caps_with ( 123 dir2. path (), 124& [ 125( "net" , false , & net_host_port ( & http_url)), 126( "net" , false , & net_host_port ( & target. url )), 127( "read" , false , & lock2), 128], 129); 130let store =store_with ( server2. options ( dir2. path ()), SourceAuthority :: read ( both)); 131assert_eq! ( 132block_on ( store. remote_source ( & name)). unwrap (), 133"export const value = 7;" 134); 135} 136 137# [ test ] 138fn execution_only_authority_never_exposes_source_in_read () { 139let http =Http :: new (); 140 http. source ( "mod.ts" , "export const value = 1;" ); 141let name = http. name ( "mod.ts" ); 142let http_url = url:: Url :: parse ( & name). unwrap (); 143 144let dir = tempfile:: tempdir (). unwrap (); 145 std:: fs:: write ( dir. path (). join ( "local.ts" ), "export const value = 2;" ). unwrap (); 146let local = dir. path (). join ( "local.ts" ). to_string_lossy (). into_owned (); 147 148// js-exec without net/read: Execute allows, Read denies everything. 149// The lock grant lets the Read store construct; fetches still deny. 150let lock = dir. path (). join ( "avril.lock" ). to_string_lossy (). into_owned (); 151let caps =caps_with ( 152 dir. path (), 153& [ 154( "js-exec" , false , & name), 155( "js-exec" , false , & local), 156( "read" , false , & lock), 157], 158); 159let execute =SourceAuthority :: execute ( caps. clone ()); 160let read =SourceAuthority :: read ( caps); 161assert! ( execute. check_remote_module ( & http_url). is_ok ()); 162assert! ( read. check_remote_module ( & http_url). is_err ()); 163 164let target =PathTarget :: resolve ( & local). unwrap (); 165assert! ( execute. check_local ( & target). is_ok ()); 166assert! ( read. check_local ( & target). is_err ()); 167 168// Store-level: Read fetch fails even though Execute would allow it. 169let server =Registry :: new (); 170let store =store_with ( server. options ( dir. path ()), read); 171assert! ( 172block_on ( store. remote_source ( & name)) 173. unwrap_err () 174. to_string () 175. contains ( "PermissionDenied" ) 176); 177} 178 179# [ test ] 180fn read_reauthorizes_cached_access_by_final_url () { 181let http =Http :: new (); 182let target =Http :: new (); 183 http. redirect ( "start" , & target. name ( "nested/mod.ts" )); 184 target. source ( "nested/mod.ts" , "export const value = 7;" ); 185let name = http. name ( "start" ); 186let http_url = url:: Url :: parse ( & name). unwrap (); 187 188let dir = tempfile:: tempdir (). unwrap (); 189let server =Registry :: new (); 190let lock = dir. path (). join ( "avril.lock" ). to_string_lossy (). into_owned (); 191let caps =caps_with ( 192 dir. path (), 193& [ 194( "net" , false , & net_host_port ( & http_url)), 195( "net" , false , & net_host_port ( & target. url )), 196( "read" , false , & lock), 197], 198); 199let store =store_with ( 200 server. options ( dir. path ()), 201SourceAuthority :: read ( caps. clone ()), 202); 203assert_eq! ( 204block_on ( store. remote_source ( & name)). unwrap (), 205"export const value = 7;" 206); 207// Revoke the final host: the cached artifact must deny on re-read. 208 caps. pledge ( vec! [( 209Category :: Net , 210None , 211vec! [ net_host_port ( & target. url )], 212)]) 213. unwrap (); 214assert! ( 215block_on ( store. remote_source ( & name)) 216. unwrap_err () 217. to_string () 218. contains ( "PermissionDenied" ) 219); 220} 221 222# [ test ] 223fn execute_revocation_denies_cached_remote_access () { 224let http =Http :: new (); 225 http. source ( "mod.ts" , "export const value = 1;" ); 226let name = http. name ( "mod.ts" ); 227 228let dir = tempfile:: tempdir (). unwrap (); 229let server =Registry :: new (); 230let caps =caps_with ( dir. path (), & [( "js-exec" , false , & name)]); 231let store =store_with ( 232 server. options ( dir. path ()), 233SourceAuthority :: execute ( caps. clone ()), 234); 235assert_eq! ( 236block_on ( store. remote_source ( & name)). unwrap (), 237"export const value = 1;" 238); 239 caps. pledge ( vec! [( Category :: JsExec , None , vec! [ name. clone ()])]) 240. unwrap (); 241assert! ( 242block_on ( store. remote_source ( & name)) 243. unwrap_err () 244. to_string () 245. contains ( "PermissionDenied" ) 246); 247} 248 249# [ test ] 250fn read_local_requires_read_grant () { 251let dir = tempfile:: tempdir (). unwrap (); 252 std:: fs:: write ( dir. path (). join ( "a.ts" ), "export default 1;" ). unwrap (); 253let path = dir. path (). join ( "a.ts" ). to_string_lossy (). into_owned (); 254let target =PathTarget :: resolve ( & path). unwrap (); 255 256let read_only =caps_with ( dir. path (), & [( "read" , false , & path)]); 257assert! ( 258SourceAuthority :: read ( read_only) 259. check_local ( & target) 260. is_ok () 261); 262 263let exec_only =caps_with ( dir. path (), & [( "js-exec" , false , & path)]); 264assert! ( 265SourceAuthority :: read ( exec_only) 266. check_local ( & target) 267. is_err () 268); 269} 270 271# [ test ] 272fn read_lockfile_needs_read_authority () { 273let http =Http :: new (); 274 http. source ( "mod.ts" , "export const value = 1;" ); 275let dir = tempfile:: tempdir (). unwrap (); 276let server =Registry :: new (); 277 std:: fs:: write ( dir. path (). join ( "avril.lock" ), "not a lockfile" ). unwrap (); 278let caps =caps_with ( dir. path (), & [( "net" , false , & net_host_port ( & http. url ))]); 279// The malformed lock is never parsed without read authority for it. 280let error =match PackageStore :: new_with_authority ( 281 registry_fixture:: runtime_handle (), 282 server. options ( dir. path ()), 283SourceAuthority :: read ( caps), 284) { 285Ok ( _) =>panic! ( "lockfile without read authority must deny" ), 286Err ( error) => error. to_string (), 287}; 288assert! ( error. contains ( "PermissionDenied" ), "{error}" ); 289} 290 291# [ test ] 292fn lockfile_rereads_respect_revocation () { 293let http =Http :: new (); 294 http. source ( "mod.ts" , "export default 1;" ); 295let server =Registry :: new (); 296for save_firstin [ false , true ] { 297let dir = tempfile:: tempdir (). unwrap (); 298let lock = dir. path (). join ( "avril.lock" ); 299let caps =caps_with ( 300 dir. path (), 301& [ 302( "read" , false , lock. to_str (). unwrap ()), 303( "net" , false , & net_host_port ( & http. url )), 304], 305); 306let store =store_with ( 307 server. options ( dir. path ()), 308SourceAuthority :: read ( caps. clone ()), 309); 310block_on ( store. remote_source ( & http. name ( "mod.ts" ))). unwrap (); 311if save_first{ 312 store. save (). unwrap (); 313} 314 caps. pledge ( vec! [( 315Category :: Read , 316None , 317vec! [ lock. to_str (). unwrap (). to_owned ()], 318)]) 319. unwrap (); 320let malformed ="{ secret: 'LOCK_SECRET'" ; 321 std:: fs:: write ( & lock, malformed). unwrap (); 322let error = store. save (). unwrap_err (). to_string (); 323assert! ( error. contains ( "PermissionDenied" ), "{error}" ); 324assert! ( !error. contains ( "LOCK_SECRET" ), "{error}" ); 325assert_eq! ( std:: fs:: read_to_string ( lock). unwrap (), malformed); 326} 327} 328 329# [ test ] 330fn npm_and_jsr_acquisition_follow_source_mode () { 331let server =Registry :: new (); 332 server. package ( 333"@test/pkg" , 334"1.0.0" , 335& [( "mod.ts" , "export const value = 1;" )], 336& [( "." , "./mod.ts" )], 337); 338 server. publish_npm ( 339"lib" , 340"1.0.0" , 341& [ 342( "package.json" , "{\"name\":\"lib\",\"version\":\"1.0.0\"}" ), 343( "index.js" , "export default 1;" ), 344], 345); 346 347// Read with net to the registry: tarball fetch, verified reads, and 348// registry sources all work. 349let dir = tempfile:: tempdir (). unwrap (); 350let net =net_host_port ( & server. url ); 351let lock = dir. path (). join ( "avril.lock" ). to_string_lossy (). into_owned (); 352let caps =caps_with ( dir. path (), & [( "net" , false , & net), ( "read" , false , & lock)]); 353let store =store_with ( 354 server. options ( dir. path ()), 355SourceAuthority :: read ( caps. clone ()), 356); 357let artifact =block_on ( store. npm_acquire ( "npm:lib@^1" )). unwrap (); 358assert_eq! ( 359 artifact. read_file ( dir. path (), "index.js" ). unwrap (), 360b"export default 1;" 361); 362let module =block_on ( store. resolve ( "jsr:@test/pkg@^1" )). unwrap (); 363assert! ( module. contains ( "/1.0.0/" )); 364assert_eq! ( 365block_on ( store. source ( & module)). unwrap (), 366"export const value = 1;" 367); 368// Revoking the registry denies settled acquisitions on reuse. 369 caps. pledge ( vec! [( Category :: Net , None , vec! [ net])]). unwrap (); 370assert! ( 371block_on ( store. npm_acquire ( "npm:lib@^1" )) 372. unwrap_err () 373. to_string () 374. contains ( "PermissionDenied" ) 375); 376assert! ( 377block_on ( store. source ( & module)) 378. unwrap_err () 379. to_string () 380. contains ( "PermissionDenied" ) 381); 382 383// Execution-only grants never satisfy Read acquisition. 384let dir2 = tempfile:: tempdir (). unwrap (); 385let lock2 = dir2 386. path () 387. join ( "avril.lock" ) 388. to_string_lossy () 389. into_owned (); 390let exec_caps =caps_with ( 391 dir2. path (), 392& [ 393( 394"js-exec" , 395false , 396& format! ( "{}@test/pkg/" , server. url . as_str ()), 397), 398( "js-exec" , false , & format! ( "{}lib/" , server. url . as_str ())), 399( "read" , false , & lock2), 400], 401); 402let store =store_with ( 403 server. options ( dir2. path ()), 404SourceAuthority :: read ( exec_caps), 405); 406assert! ( 407block_on ( store. npm_acquire ( "npm:lib@^1" )) 408. unwrap_err () 409. to_string () 410. contains ( "PermissionDenied" ) 411); 412assert! ( 413block_on ( store. resolve ( "jsr:@test/pkg@^1" )) 414. unwrap_err () 415. to_string () 416. contains ( "PermissionDenied" ) 417); 418} 419 420# [ test ] 421fn nested_local_configs_follow_source_mode () { 422let dir = tempfile:: tempdir (). unwrap (); 423let root = dir. path (); 424let dep = root. join ( "dep" ); 425 std:: fs:: create_dir_all ( & dep). unwrap (); 426 std:: fs:: write ( 427 root. join ( "avril.conf" ), 428"deps: [{from: './dep', name: 'dep'}]" , 429) 430. unwrap (); 431 std:: fs:: write ( 432 dep. join ( "avril.conf" ), 433"name: 'dep', exports: {'.': './mod.ts'}" , 434) 435. unwrap (); 436 std:: fs:: write ( dep. join ( "mod.ts" ), "export const value = 1;" ). unwrap (); 437 std:: fs:: write ( root. join ( "main.ts" ), "import 'dep';" ). unwrap (); 438let main = root. join ( "main.ts" ). to_string_lossy (). into_owned (); 439let dep_mod = dep. join ( "mod.ts" ). to_string_lossy (). into_owned (); 440let root_str = root. to_string_lossy (). into_owned (); 441let lock = root. join ( "avril.lock" ). to_string_lossy (). into_owned (); 442 443// Execute resolves through both configs with exact import mapping. 444let caps =caps_with ( root, & [( "js-exec" , false , & root_str)]); 445let graph =ModuleGraph :: open ( 446 registry_fixture:: runtime_handle (), 447SourceAuthority :: execute ( caps), 448graph_options ( root), 449) 450. unwrap (); 451assert_eq! ( 452block_on ( graph. prepare ( main. clone (), String :: new ())). unwrap (), 453 main 454); 455let info = graph. info ( & main). unwrap (); 456assert_eq! ( 457 info[ "modules" ][ & main][ "imports" ][ "dep" ], 458 serde_json:: json!( dep_mod) 459); 460assert_eq! ( 461block_on ( graph. original_source ( & dep_mod)). unwrap (), 462"export const value = 1;" 463); 464 465// Read with only execution grants exposes nothing. 466let exec_only =caps_with ( 467 root, 468& [( "js-exec" , false , & root_str), ( "read" , false , & lock)], 469); 470let graph =ModuleGraph :: open ( 471 registry_fixture:: runtime_handle (), 472SourceAuthority :: read ( exec_only), 473graph_options ( root), 474) 475. unwrap (); 476assert! ( 477block_on ( graph. prepare ( main. clone (), String :: new ())) 478. unwrap_err () 479. to_string () 480. contains ( "PermissionDenied" ) 481); 482 483// Read with read grants resolves the same nesting. 484let read_caps =caps_with ( root, & [( "read" , false , & root_str)]); 485let graph =ModuleGraph :: open ( 486 registry_fixture:: runtime_handle (), 487SourceAuthority :: read ( read_caps), 488graph_options ( root), 489) 490. unwrap (); 491assert_eq! ( 492block_on ( graph. prepare ( main. clone (), String :: new ())). unwrap (), 493 main 494); 495let info = graph. info ( & main). unwrap (); 496assert_eq! ( 497 info[ "modules" ][ & main][ "imports" ][ "dep" ], 498 serde_json:: json!( dep_mod) 499); 500} 501 502# [ test ] 503fn module_graph_read_isolates_execution_only_source () { 504let dir = tempfile:: tempdir (). unwrap (); 505let root = dir. path (); 506 std:: fs:: write ( root. join ( "avril.conf" ), "" ). unwrap (); 507 std:: fs:: write ( root. join ( "main.ts" ), "export const value = 1;" ). unwrap (); 508let main = root. join ( "main.ts" ). to_string_lossy (). into_owned (); 509let lock = root. join ( "avril.lock" ). to_string_lossy (). into_owned (); 510 511let caps =caps_with ( root, & [( "js-exec" , false , & main), ( "read" , false , & lock)]); 512let graph =ModuleGraph :: open ( 513 registry_fixture:: runtime_handle (), 514SourceAuthority :: execute ( caps. clone ()), 515graph_options ( root), 516) 517. unwrap (); 518block_on ( graph. prepare ( main. clone (), String :: new ())). unwrap (); 519assert_eq! ( 520block_on ( graph. original_source ( & main)). unwrap (), 521"export const value = 1;" 522); 523 524// Same caps in Read mode expose nothing. 525let graph =ModuleGraph :: open ( 526 registry_fixture:: runtime_handle (), 527SourceAuthority :: read ( caps), 528graph_options ( root), 529) 530. unwrap (); 531assert! ( 532block_on ( graph. original_source ( & main)) 533. unwrap_err () 534. to_string () 535. contains ( "PermissionDenied" ) 536); 537} 538 539# [ test ] 540fn redirected_graph_load_reauthorizes_final_host () { 541let http =Http :: new (); 542let target =Http :: new (); 543 http. redirect ( "start" , & target. name ( "mod.ts" )); 544 target. source ( "mod.ts" , "export const value = 7;" ); 545let name = http. name ( "start" ); 546 547let dir = tempfile:: tempdir (). unwrap (); 548let root = dir. path (); 549let lock = root. join ( "avril.lock" ). to_string_lossy (). into_owned (); 550let caps =caps_with ( 551 root, 552& [ 553( "net" , false , & net_host_port ( & http. url )), 554( "net" , false , & net_host_port ( & target. url )), 555( "read" , false , & lock), 556], 557); 558let graph =ModuleGraph :: open ( 559 registry_fixture:: runtime_handle (), 560SourceAuthority :: read ( caps. clone ()), 561graph_options ( root), 562) 563. unwrap (); 564block_on ( graph. prepare ( name. clone (), String :: new ())). unwrap (); 565assert_eq! ( 566block_on ( graph. original_source ( & name)). unwrap (), 567"export const value = 7;" 568); 569// Revoking the final host denies the cached record as well as refetch. 570 caps. pledge ( vec! [( 571Category :: Net , 572None , 573vec! [ net_host_port ( & target. url )], 574)]) 575. unwrap (); 576assert! ( 577 graph 578. load ( & name) 579. unwrap_err () 580. to_string () 581. contains ( "PermissionDenied" ) 582); 583assert! ( 584block_on ( graph. original_source ( & name)) 585. unwrap_err () 586. to_string () 587. contains ( "PermissionDenied" ) 588); 589} 590 591# [ test ] 592fn child_graph_revocation_denies_despite_parent_store () { 593let http =Http :: new (); 594 http. source ( "mod.ts" , "export const value = 1;" ); 595let name = http. name ( "mod.ts" ); 596 597let dir = tempfile:: tempdir (). unwrap (); 598let root = dir. path (); 599let parent =caps_with ( root, & [( "js-exec" , false , & name)]); 600let store =Arc :: new ( 601PackageStore :: new_with_authority ( 602 registry_fixture:: runtime_handle (), 603graph_options ( root), 604SourceAuthority :: execute ( parent. clone ()), 605) 606. unwrap (), 607); 608let child =Capabilities :: inherit ( parent. clone ()); 609let graph =ModuleGraph :: open_shared ( SourceAuthority :: execute ( child. clone ()), store); 610block_on ( graph. prepare ( name. clone (), String :: new ())). unwrap (); 611assert_eq! ( 612block_on ( graph. original_source ( & name)). unwrap (), 613"export const value = 1;" 614); 615// Revoking only the child denies the cached load. 616 child 617. pledge ( vec! [( Category :: JsExec , None , vec! [ name. clone ()])]) 618. unwrap (); 619assert! ( 620 graph 621. load ( & name) 622. unwrap_err () 623. to_string () 624. contains ( "PermissionDenied" ) 625); 626} 627 628# [ test ] 629fn nested_remote_dependency_follows_source_mode () { 630let front =Http :: new (); 631let target =Http :: new (); 632// The requested child config redirects to its final host. 633 front. redirect ( "dep/avril.conf" , & target. name ( "avril.conf" )); 634let child_url = front. url . join ( "dep/" ). unwrap (); 635let child_mod = target. name ( "mod.ts" ); 636 target. source ( 637"avril.conf" , 638"name: \"dep\"\nexports: {\".\": \"./mod.ts\"}" , 639); 640 target. source ( "mod.ts" , "export const value = 1;" ); 641 642let dir = tempfile:: tempdir (). unwrap (); 643let root = dir. path (); 644 std:: fs:: write ( 645 root. join ( "avril.conf" ), 646format! ( "deps: [{{from: \"{}\", name: 'dep'}}]" , child_url. as_str ()), 647) 648. unwrap (); 649 std:: fs:: write ( root. join ( "main.ts" ), "import 'dep';" ). unwrap (); 650let main = root. join ( "main.ts" ). to_string_lossy (). into_owned (); 651 652// Read with net for both hosts traverses the nesting. 653let net_caps =caps_with ( 654 root, 655& [ 656( "net" , false , & net_host_port ( & front. url )), 657( "net" , false , & net_host_port ( & target. url )), 658( "read" , false , root. to_str (). unwrap ()), 659], 660); 661let graph =ModuleGraph :: open ( 662 registry_fixture:: runtime_handle (), 663SourceAuthority :: read ( net_caps. clone ()), 664graph_options ( root), 665) 666. unwrap (); 667assert_eq! ( 668block_on ( graph. prepare ( main. clone (), String :: new ())). unwrap (), 669 main 670); 671assert_eq! ( 672block_on ( graph. original_source ( & child_mod)). unwrap (), 673"export const value = 1;" 674); 675 676let requests =( 677 front. requests . lock (). unwrap (). clone (), 678 target. requests . lock (). unwrap (). clone (), 679); 680// Revoking only the final host denies cached and fresh reads alike 681// while the requested host stays granted. 682 net_caps 683. pledge ( vec! [( 684Category :: Net , 685None , 686vec! [ net_host_port ( & target. url )], 687)]) 688. unwrap (); 689assert! ( 690 graph 691. load ( & child_mod) 692. unwrap_err () 693. to_string () 694. contains ( "PermissionDenied" ) 695); 696assert! ( 697block_on ( graph. original_source ( & child_mod)) 698. unwrap_err () 699. to_string () 700. contains ( "PermissionDenied" ) 701); 702 703assert! ( 704block_on ( graph. initialize_package ( & child_mod)) 705. unwrap_err () 706. to_string () 707. contains ( "PermissionDenied" ) 708); 709assert_eq! ( * front. requests . lock (). unwrap (), requests. 0 ); 710assert_eq! ( * target. requests . lock (). unwrap (), requests. 1 ); 711 712// Local reads do not authorize remote dependencies with execution-only grants. 713let dir2 = tempfile:: tempdir (). unwrap (); 714let root2 = dir2. path (); 715 std:: fs:: write ( 716 root2. join ( "avril.conf" ), 717format! ( "deps: [{{from: \"{}\", name: 'dep'}}]" , child_url. as_str ()), 718) 719. unwrap (); 720 std:: fs:: write ( root2. join ( "main.ts" ), "import 'dep';" ). unwrap (); 721let main2 = root2. join ( "main.ts" ). to_string_lossy (). into_owned (); 722let exec_only =caps_with ( 723 root2, 724& [ 725( "js-exec" , false , & front. name ( "dep/avril.conf" )), 726( "js-exec" , false , target. url . as_str ()), 727( "read" , false , root2. to_str (). unwrap ()), 728], 729); 730let graph =ModuleGraph :: open ( 731 registry_fixture:: runtime_handle (), 732SourceAuthority :: read ( exec_only), 733graph_options ( root2), 734) 735. unwrap (); 736let error =match block_on ( graph. prepare ( main2, String :: new ())) { 737Ok ( _) =>panic! ( "execution-only authority must deny nested export" ), 738Err ( error) => error. to_string (), 739}; 740assert! ( error. contains ( "PermissionDenied: net" ), "{error}" ); 741assert_eq! ( * front. requests . lock (). unwrap (), requests. 0 ); 742assert_eq! ( * target. requests . lock (). unwrap (), requests. 1 ); 743} 744 745# [ test ] 746fn hidden_malformed_path_dependency_stays_hidden_in_read () { 747const MARKER : & str ="SECRET_MARKER_9f3c" ; 748let dir = tempfile:: tempdir (). unwrap (); 749let root = dir. path (); 750 std:: fs:: write ( root. join ( "avril.conf" ), "deps: ['./secret']" ). unwrap (); 751 std:: fs:: write ( root. join ( "main.ts" ), "import 'lib';" ). unwrap (); 752let secret = root. join ( "secret" ); 753 std:: fs:: create_dir_all ( & secret). unwrap (); 754 std:: fs:: write ( secret. join ( "avril.conf" ), format! ( ": {{{{ {MARKER} oops" )). unwrap (); 755 std:: fs:: write ( secret. join ( "mod.ts" ), "export default 1;" ). unwrap (); 756let main = root. join ( "main.ts" ). to_string_lossy (). into_owned (); 757let root_str = root. to_string_lossy (). into_owned (); 758let secret_str = secret. to_string_lossy (). into_owned (); 759 760// Read sees the parent but not the hidden dependency: no marker leaks. 761let caps =caps_with ( 762 root, 763& [( "read" , false , & root_str), ( "read" , true , & secret_str)], 764); 765let graph =ModuleGraph :: open ( 766 registry_fixture:: runtime_handle (), 767SourceAuthority :: read ( caps), 768graph_options ( root), 769) 770. unwrap (); 771let error =match block_on ( graph. prepare ( main. clone (), String :: new ())) { 772Ok ( _) =>panic! ( "hidden dependency must deny" ), 773Err ( error) => error. to_string (), 774}; 775assert! ( error. contains ( "PermissionDenied" ), "{error}" ); 776assert! ( !error. contains ( MARKER ), "{error}" ); 777 778// With the dependency granted, its declared name resolves. 779let dir2 = tempfile:: tempdir (). unwrap (); 780let root2 = dir2. path (); 781 std:: fs:: write ( root2. join ( "avril.conf" ), "deps: ['./secret']" ). unwrap (); 782 std:: fs:: write ( root2. join ( "main.ts" ), "import 'lib';" ). unwrap (); 783let secret2 = root2. join ( "secret" ); 784 std:: fs:: create_dir_all ( & secret2). unwrap (); 785 std:: fs:: write ( 786 secret2. join ( "avril.conf" ), 787"name: 'lib', exports: {'.': './mod.ts'}" , 788) 789. unwrap (); 790 std:: fs:: write ( secret2. join ( "mod.ts" ), "export default 1;" ). unwrap (); 791let main2 = root2. join ( "main.ts" ). to_string_lossy (). into_owned (); 792let root2_str = root2. to_string_lossy (). into_owned (); 793let caps2 =caps_with ( root2, & [( "read" , false , & root2_str)]); 794let graph =ModuleGraph :: open ( 795 registry_fixture:: runtime_handle (), 796SourceAuthority :: read ( caps2), 797graph_options ( root2), 798) 799. unwrap (); 800assert_eq! ( 801block_on ( graph. prepare ( main2. clone (), String :: new ())). unwrap (), 802 main2 803); 804let info = graph. info ( & main2). unwrap (); 805assert! ( info[ "modules" ][ & main2][ "imports" ][ "lib" ]. is_string ()); 806} 807 808# [ test ] 809fn local_revocation_denies_cached_config_access () { 810let dir = tempfile:: tempdir (). unwrap (); 811let root = dir. path (); 812 std:: fs:: write ( root. join ( "avril.conf" ), "name: 'app'" ). unwrap (); 813 std:: fs:: write ( root. join ( "main.ts" ), "export default 1;" ). unwrap (); 814let main = root. join ( "main.ts" ). to_string_lossy (). into_owned (); 815let root_str = root. to_string_lossy (). into_owned (); 816let conf = root. join ( "avril.conf" ). to_string_lossy (). into_owned (); 817 818let caps =caps_with ( root, & [( "read" , false , & root_str)]); 819let graph =ModuleGraph :: open ( 820 registry_fixture:: runtime_handle (), 821SourceAuthority :: read ( caps. clone ()), 822graph_options ( root), 823) 824. unwrap (); 825block_on ( graph. prepare ( main. clone (), String :: new ())). unwrap (); 826assert! ( graph. info ( & main). unwrap ()[ "modules" ][ & main][ "package" ]. is_object ()); 827let src = root. join ( "src" ); 828 std:: fs:: create_dir ( & src). unwrap (); 829let nested = src. join ( "main.ts" ); 830let authority =SourceAuthority :: read ( caps. clone ()); 831assert_eq! ( 832ModuleGraph :: project_root ( & authority, nested. to_str (). unwrap ()). unwrap (), 833 root 834); 835 caps. pledge ( vec! [( Category :: Read , None , vec! [ conf])]) 836. unwrap (); 837// The same graph hides the package once its config is revoked. 838assert! ( graph. info ( & main). unwrap ()[ "modules" ][ & main][ "package" ]. is_null ()); 839assert_eq! ( 840ModuleGraph :: project_root ( & authority, nested. to_str (). unwrap ()). unwrap (), 841 src 842); 843} 844 845# [ test ] 846fn graph_factory_builds_consistent_authority () { 847let dir = tempfile:: tempdir (). unwrap (); 848let root = dir. path (); 849 std:: fs:: write ( root. join ( "avril.conf" ), "" ). unwrap (); 850 std:: fs:: write ( root. join ( "main.ts" ), "export const value = 1;" ). unwrap (); 851let main = root. join ( "main.ts" ). to_string_lossy (). into_owned (); 852let lock = root. join ( "avril.lock" ). to_string_lossy (). into_owned (); 853let root_str = root. to_string_lossy (). into_owned (); 854 855// Execute opens and prepares with execution grants. 856let caps =caps_with ( root, & [( "js-exec" , false , & root_str)]); 857let graph =ModuleGraph :: open ( 858 registry_fixture:: runtime_handle (), 859SourceAuthority :: execute ( caps), 860graph_options ( root), 861) 862. unwrap (); 863assert_eq! ( 864block_on ( graph. prepare ( main. clone (), String :: new ())). unwrap (), 865 main 866); 867 868// Read opens and prepares with read grants. 869let read_caps =caps_with ( root, & [( "read" , false , & root_str)]); 870let graph =ModuleGraph :: open ( 871 registry_fixture:: runtime_handle (), 872SourceAuthority :: read ( read_caps), 873graph_options ( root), 874) 875. unwrap (); 876assert_eq! ( 877block_on ( graph. prepare ( main. clone (), String :: new ())). unwrap (), 878 main 879); 880 881// Read construction without lock-read authority denies up front. 882let bare =caps_with ( root, & [( "net" , false , "example.invalid:443" )]); 883match ModuleGraph :: open ( 884 registry_fixture:: runtime_handle (), 885SourceAuthority :: read ( bare), 886graph_options ( root), 887) { 888Ok ( _) =>panic! ( "lock without read authority must deny" ), 889Err ( error) =>assert! ( error. to_string (). contains ( "PermissionDenied" ), "{error}" ), 890} 891 892// Execution-only sources never satisfy a Read graph. 893let exec_only =caps_with ( 894 root, 895& [( "js-exec" , false , & root_str), ( "read" , false , & lock)], 896); 897let graph =ModuleGraph :: open ( 898 registry_fixture:: runtime_handle (), 899SourceAuthority :: read ( exec_only), 900graph_options ( root), 901) 902. unwrap (); 903assert! ( 904block_on ( graph. prepare ( main. clone (), String :: new ())) 905. unwrap_err () 906. to_string () 907. contains ( "PermissionDenied" ) 908); 909}