feat(core): Add permission-controlled Deno execution, configurable script server port, improved downloader, dependency bumps and test updates
This commit is contained in:
@@ -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.'
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export * from './classes.smartdeno.js';
|
||||
export type { TDenoPermission } from './classes.denoexecution.js';
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user