feat(core): Add permission-controlled Deno execution, configurable script server port, improved downloader, dependency bumps and test updates

This commit is contained in:
2025-12-02 11:27:35 +00:00
parent 01dd40e599
commit fb0bfed4ab
20 changed files with 7919 additions and 3613 deletions

View File

@@ -1,8 +1,8 @@
/**
* autocreated commitinfo by @pushrocks/commitinfo
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@push.rocks/smartdeno',
version: '1.0.3',
description: 'a module to run deno from node'
version: '1.1.0',
description: 'A module to run Deno scripts from Node.js, including functionalities for downloading Deno and executing Deno scripts.'
}

View File

@@ -14,9 +14,11 @@ interface IAsset {
}
export class DenoDownloader {
private denoBinaryPath: string | null = null;
private async getDenoDownloadUrl(): Promise<string> {
const osPlatform = platform(); // 'darwin', 'linux', 'win32', etc.
const arch = process.arch; // 'x64', 'arm64', etc.
const osPlatform = platform();
const arch = process.arch;
let osPart: string;
switch (osPlatform) {
@@ -36,9 +38,12 @@ export class DenoDownloader {
const archPart = arch === 'x64' ? 'x86_64' : 'aarch64';
const releasesResponse = await fetch('https://api.github.com/repos/denoland/deno/releases/latest');
if (!releasesResponse.ok) {
throw new Error(`Failed to fetch Deno releases: ${releasesResponse.statusText}`);
}
const release: IDenoRelease = await releasesResponse.json();
const executableName = `deno-${archPart}-${osPart}.zip`; // Adjust if naming convention changes
const executableName = `deno-${archPart}-${osPart}.zip`;
const asset = release.assets.find(a => a.name === executableName);
if (!asset) {
@@ -57,24 +62,60 @@ export class DenoDownloader {
await fs.writeFile(outputPath, Buffer.from(buffer));
}
public async download(outputPath: string = './deno.zip'): Promise<void> {
try {
const url = await this.getDenoDownloadUrl();
await this.downloadDeno(url, outputPath);
console.log(`Deno downloaded successfully to ${outputPath}`);
} catch (error) {
console.error(`Error downloading Deno: ${error.message}`);
}
if (await plugins.smartfile.fs.fileExists(plugins.path.join(paths.nogitDir, 'deno'))) {
return;
}
const smartarchive = await plugins.smartarchive.SmartArchive.fromArchiveFile(outputPath);
/**
* Get the path to the Deno binary after download
*/
public getDenoBinaryPath(): string | null {
return this.denoBinaryPath;
}
/**
* Download and extract Deno to the specified directory
* @param outputPath Path where the deno.zip will be downloaded
* @returns Path to the Deno binary
*/
public async download(outputPath: string = './deno.zip'): Promise<string> {
const fsInstance = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
const directory = plugins.path.dirname(outputPath);
const denoBinaryPath = plugins.path.join(directory, platform() === 'win32' ? 'deno.exe' : 'deno');
// Check if Deno is already downloaded
if (await fsInstance.file(denoBinaryPath).exists()) {
console.log(`Deno already exists at ${denoBinaryPath}`);
this.denoBinaryPath = denoBinaryPath;
return denoBinaryPath;
}
// Ensure the directory exists
await fsInstance.directory(directory).create();
// Download Deno
const url = await this.getDenoDownloadUrl();
await this.downloadDeno(url, outputPath);
console.log(`Deno downloaded successfully to ${outputPath}`);
// Extract the archive
console.log(`Extracting deno.zip to ${directory}`);
await smartarchive.exportToFs(directory);
const smartshellInstance = new plugins.smarthshell.Smartshell({
executor: 'bash'
});
await smartshellInstance.exec(`(cd ${paths.nogitDir} && chmod +x deno)`);
await plugins.smartarchive.SmartArchive.create()
.file(outputPath)
.extract(directory);
// Make the binary executable (Unix-like systems)
if (platform() !== 'win32') {
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash'
});
await smartshellInstance.exec(`chmod +x "${denoBinaryPath}"`);
}
// Clean up the zip file
try {
await fsInstance.file(outputPath).delete();
} catch {
// Ignore cleanup errors
}
this.denoBinaryPath = denoBinaryPath;
return denoBinaryPath;
}
}

View File

@@ -1,21 +1,69 @@
import type { ScriptServer } from './classes.scriptserver.js';
import * as plugins from './plugins.js';
/* This file contains logic to execute deno commands in an ephermal way */
export type TDenoPermission =
| 'all'
| 'env'
| 'ffi'
| 'hrtime'
| 'net'
| 'read'
| 'run'
| 'sys'
| 'write';
export interface IDenoExecutionOptions {
permissions?: TDenoPermission[];
denoBinaryPath?: string;
}
/**
* This class contains logic to execute deno commands in an ephemeral way
*/
export class DenoExecution {
public id: string;
public scriptserverRef: ScriptServer;
public script: string;
public options: IDenoExecutionOptions;
constructor(scriptserverRef: ScriptServer, scriptArg: string) {
constructor(scriptserverRef: ScriptServer, scriptArg: string, options: IDenoExecutionOptions = {}) {
this.scriptserverRef = scriptserverRef;
this.script = scriptArg;
this.options = options;
this.id = plugins.smartunique.shortId();
}
public async execute() {
this.scriptserverRef.executionMap.add(this);
await this.scriptserverRef.smartshellInstance.exec(`deno run http://localhost:3210/getscript/${this.id}`)
private buildPermissionFlags(): string {
const permissions = this.options.permissions || [];
if (permissions.length === 0) {
return '';
}
if (permissions.includes('all')) {
return '-A';
}
return permissions.map(p => `--allow-${p}`).join(' ');
}
}
public async execute(): Promise<{ exitCode: number; stdout: string; stderr: string }> {
this.scriptserverRef.executionMap.add(this);
try {
const denoBinary = this.options.denoBinaryPath || 'deno';
const permissionFlags = this.buildPermissionFlags();
const port = this.scriptserverRef.getPort();
const scriptUrl = `http://localhost:${port}/getscript/${this.id}`;
const command = `${denoBinary} run ${permissionFlags} ${scriptUrl}`.replace(/\s+/g, ' ').trim();
const result = await this.scriptserverRef.smartshellInstance.exec(command);
return {
exitCode: result.exitCode,
stdout: result.stdout,
stderr: result.stderr,
};
} finally {
// Clean up: remove from execution map after execution completes
this.scriptserverRef.executionMap.remove(this);
}
}
}

View File

@@ -1,17 +1,30 @@
import type { DenoExecution } from './classes.denoexecution.js';
import * as plugins from './plugins.js';
export interface IScriptServerOptions {
port?: number;
}
export class ScriptServer {
private server: plugins.typedserver.servertools.Server;
public smartshellInstance = new plugins.smarthshell.Smartshell({
private port: number;
public smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash'
});
public executionMap = new plugins.lik.ObjectMap<DenoExecution>();
constructor(options: IScriptServerOptions = {}) {
this.port = options.port ?? 3210;
}
public getPort(): number {
return this.port;
}
public async start() {
this.server = new plugins.typedserver.servertools.Server({
port: 3210,
port: this.port,
cors: true,
});
this.server.addRoute(
@@ -20,12 +33,24 @@ export class ScriptServer {
const executionId = req.params.executionId;
const denoExecution = await this.executionMap.find(async denoExecutionArg => {
return denoExecutionArg.id === executionId;
})
});
if (!denoExecution) {
res.statusCode = 404;
res.write('Execution not found');
res.end();
return;
}
res.write(denoExecution.script);
res.end();
})
);
await this.server.start();
}
public async stop() {}
public async stop() {
if (this.server) {
await this.server.stop();
}
this.executionMap.wipe();
}
}

View File

@@ -2,33 +2,102 @@ import { DenoDownloader } from './classes.denodownloader.js';
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { ScriptServer } from './classes.scriptserver.js';
import { DenoExecution } from './classes.denoexecution.js';
import { DenoExecution, type TDenoPermission } from './classes.denoexecution.js';
export interface ISmartDenoOptions {
/**
* Force downloading a local copy of Deno even if it's available in PATH
*/
forceLocalDeno?: boolean;
/**
* Port for the internal script server (default: 3210)
*/
port?: number;
}
export interface IExecuteScriptOptions {
/**
* Deno permissions to grant to the script
*/
permissions?: TDenoPermission[];
}
export class SmartDeno {
private denoDownloader = new DenoDownloader();
private scriptServer = new ScriptServer();
private scriptServer: ScriptServer;
private denoBinaryPath: string | null = null;
private isStarted = false;
public async start(optionsArg: {
forceLocalDeno?: boolean;
} = {}) {
const denoAlreadyInPath = await plugins.smarthshell.which('deno', {
nothrow: true
});
if (!denoAlreadyInPath || optionsArg.forceLocalDeno) {
await this.denoDownloader.download(plugins.path.join(paths.nogitDir, 'deno.zip'));
}
await this.scriptServer.start();
constructor() {
this.scriptServer = new ScriptServer();
}
/**
* Stops the smartdeno instance
* Starts the SmartDeno instance
* @param optionsArg Configuration options
*/
public async stop() {
public async start(optionsArg: ISmartDenoOptions = {}): Promise<void> {
if (this.isStarted) {
return;
}
// Create script server with configured port
this.scriptServer = new ScriptServer({ port: optionsArg.port });
const denoAlreadyInPath = await plugins.smartshell.which('deno', {
nothrow: true
});
if (!denoAlreadyInPath || optionsArg.forceLocalDeno) {
this.denoBinaryPath = await this.denoDownloader.download(
plugins.path.join(paths.nogitDir, 'deno.zip')
);
} else {
this.denoBinaryPath = 'deno';
}
await this.scriptServer.start();
this.isStarted = true;
}
public async executeScript(scriptArg: string) {
const denoExecution = new DenoExecution(this.scriptServer, scriptArg);
await denoExecution.execute();
/**
* Stops the SmartDeno instance and cleans up resources
*/
public async stop(): Promise<void> {
if (!this.isStarted) {
return;
}
await this.scriptServer.stop();
this.isStarted = false;
}
}
/**
* Check if the SmartDeno instance is running
*/
public isRunning(): boolean {
return this.isStarted;
}
/**
* Execute a Deno script
* @param scriptArg The script content to execute
* @param options Execution options including permissions
* @returns Execution result with exitCode, stdout, and stderr
*/
public async executeScript(
scriptArg: string,
options: IExecuteScriptOptions = {}
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
if (!this.isStarted) {
throw new Error('SmartDeno is not started. Call start() first.');
}
const denoExecution = new DenoExecution(this.scriptServer, scriptArg, {
permissions: options.permissions,
denoBinaryPath: this.denoBinaryPath || undefined,
});
return denoExecution.execute();
}
}

View File

@@ -1,3 +1,2 @@
import * as plugins from './plugins.js';
export * from './classes.smartdeno.js';
export type { TDenoPermission } from './classes.denoexecution.js';

View File

@@ -15,16 +15,16 @@ export {
// @push.rocks scope
import * as lik from '@push.rocks/lik';
import * as smartarchive from '@push.rocks/smartarchive';
import * as smartfile from '@push.rocks/smartfile';
import * as smartfs from '@push.rocks/smartfs';
import * as smartpath from '@push.rocks/smartpath';
import * as smarthshell from '@push.rocks/smartshell';
import * as smartshell from '@push.rocks/smartshell';
import * as smartunique from '@push.rocks/smartunique';
export {
lik,
smartarchive,
smartfile,
smartfs,
smartpath,
smarthshell,
smartshell,
smartunique,
}