@@ -308,22 +308,51 @@ export function contentHash(source: string): string {
308308 return createHash ( "sha256" ) . update ( source , "utf8" ) . digest ( "hex" ) ;
309309}
310310
311+ /**
312+ * Resolve the source file for a plugin directory.
313+ *
314+ * In dev mode (source repo), prefers .ts so edits are reflected immediately
315+ * without a rebuild. Under node_modules (npm install / bundled binary),
316+ * Node.js refuses to strip types from .ts files, so we always use .js.
317+ */
318+ export function resolvePluginSource ( pluginDir : string ) : string {
319+ const tsPath = join ( pluginDir , "index.ts" ) ;
320+ const jsPath = join ( pluginDir , "index.js" ) ;
321+
322+ // Under node_modules, Node.js can't type-strip .ts — always use .js.
323+ // Use path-segment check to avoid false positives on dirs named "node_modules_foo".
324+ const underNodeModules =
325+ / [ \\ / ] n o d e _ m o d u l e s [ \\ / ] / . test ( pluginDir ) ||
326+ pluginDir . startsWith ( "node_modules/" ) ;
327+ if ( underNodeModules ) {
328+ return jsPath ;
329+ }
330+
331+ // Dev mode: prefer .ts for live editing, fall back to .js
332+ return existsSync ( tsPath ) ? tsPath : jsPath ;
333+ }
334+
311335/**
312336 * Compute combined hash of plugin source and manifest.
313337 * Used for approval fingerprint and tamper detection.
314338 * Any change to either file invalidates the approval.
339+ *
340+ * Note: approvals are scoped to the install context. A plugin approved
341+ * in dev (from .ts) must be re-approved when loaded from node_modules
342+ * (.js), since the source content differs. This is intentional —
343+ * the compiled output should be verified independently.
315344 */
316345export function computePluginHash ( pluginDir : string ) : string | null {
317- const tsPath = join ( pluginDir , "index.ts" ) ;
346+ const sourcePath = resolvePluginSource ( pluginDir ) ;
318347 const jsonPath = join ( pluginDir , "plugin.json" ) ;
319348
320349 try {
321- const tsContent = readFileSync ( tsPath , "utf8" ) ;
350+ const sourceContent = readFileSync ( sourcePath , "utf8" ) ;
322351 const jsonContent = readFileSync ( jsonPath , "utf8" ) ;
323352
324353 // Hash both files together — any change invalidates
325354 return createHash ( "sha256" )
326- . update ( tsContent , "utf8" )
355+ . update ( sourceContent , "utf8" )
327356 . update ( jsonContent , "utf8" )
328357 . digest ( "hex" ) ;
329358 } catch {
@@ -648,16 +677,17 @@ export function createPluginManager(pluginsDir: string) {
648677 // ── Source Loading ────────────────────────────────────────────
649678
650679 /**
651- * Load the source code of a plugin's index.js for auditing.
680+ * Load the source code of a plugin for auditing.
681+ * Resolves .ts (dev) or .js (npm/dist) via resolvePluginSource().
652682 * Returns the source string, or null if the file doesn't exist.
653683 */
654684 function loadSource ( name : string ) : string | null {
655685 const plugin = plugins . get ( name ) ;
656686 if ( ! plugin ) return null ;
657687
658- const indexPath = join ( plugin . dir , "index.ts" ) ;
688+ const indexPath = resolvePluginSource ( plugin . dir ) ;
659689 if ( ! existsSync ( indexPath ) ) {
660- console . error ( `[plugins] Warning: ${ name } /index.ts not found` ) ;
690+ console . error ( `[plugins] Warning: ${ indexPath } not found` ) ;
661691 return null ;
662692 }
663693
@@ -667,7 +697,7 @@ export function createPluginManager(pluginsDir: string) {
667697 return source ;
668698 } catch ( err ) {
669699 console . error (
670- `[plugins] Warning: failed to read ${ name } /index.ts : ${ ( err as Error ) . message } ` ,
700+ `[plugins] Warning: failed to read ${ name } source : ${ ( err as Error ) . message } ` ,
671701 ) ;
672702 return null ;
673703 }
@@ -804,7 +834,7 @@ export function createPluginManager(pluginsDir: string) {
804834 * Approve a plugin. Requires an existing audit result.
805835 * Persists the approval to disk immediately.
806836 *
807- * Uses combined hash (index.ts + plugin.json) for tamper detection.
837+ * Uses combined hash (plugin source + plugin.json) for tamper detection.
808838 *
809839 * @returns true if approved, false if plugin not found or not audited
810840 */
@@ -851,7 +881,7 @@ export function createPluginManager(pluginsDir: string) {
851881
852882 /**
853883 * Check if a plugin has a valid, current approval.
854- * Compares the stored content hash against combined hash (index.ts + plugin.json).
884+ * Compares the stored content hash against combined hash (plugin source + plugin.json).
855885 */
856886 function isApproved ( name : string ) : boolean {
857887 const plugin = plugins . get ( name ) ;
@@ -869,7 +899,7 @@ export function createPluginManager(pluginsDir: string) {
869899
870900 /**
871901 * Refresh the `approved` flag on all plugins based on the
872- * persisted approval store and current combined hash (index.ts + plugin.json).
902+ * persisted approval store and current combined hash (plugin source + plugin.json).
873903 * Called after discover() to sync runtime flags with disk state.
874904 */
875905 function refreshAllApprovals ( ) : void {
@@ -1167,7 +1197,7 @@ export function createPluginManager(pluginsDir: string) {
11671197 const plugin = plugins . get ( name ) ;
11681198 if ( ! plugin || ! plugin . source ) return false ;
11691199
1170- const indexPath = join ( plugin . dir , "index.ts" ) ;
1200+ const indexPath = resolvePluginSource ( plugin . dir ) ;
11711201 try {
11721202 const currentSource = readFileSync ( indexPath , "utf8" ) ;
11731203 return currentSource === plugin . source ;
0 commit comments