* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix * fix: track native source modules with root-anchored runtime ignores
113 lines
4.1 KiB
JavaScript
113 lines
4.1 KiB
JavaScript
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'fs';
|
|
import { dirname, join, resolve, sep } from 'path';
|
|
import { PlatformError } from '../../kernel/errors.mjs';
|
|
import { validateModulePackage } from './module-package-contract.mjs';
|
|
|
|
function ensureInside(root, pathname) {
|
|
const base = resolve(root);
|
|
const target = resolve(pathname);
|
|
if (target !== base && !target.startsWith(`${base}${sep}`)) {
|
|
throw new PlatformError('Moduldatei liegt außerhalb des Modulspeichers.', { code: 'MODULE_STORAGE_ESCAPE', status: 400, expose: true });
|
|
}
|
|
return target;
|
|
}
|
|
|
|
export class ModulePackageStore {
|
|
#root;
|
|
#trustKeys;
|
|
|
|
constructor(root, { trustKeys = {} } = {}) {
|
|
this.#root = resolve(root);
|
|
this.#trustKeys = { ...trustKeys };
|
|
mkdirSync(this.#root, { recursive: true });
|
|
}
|
|
|
|
#moduleRoot(id) {
|
|
return ensureInside(this.#root, join(this.#root, id));
|
|
}
|
|
|
|
#packagePath(id) {
|
|
return join(this.#moduleRoot(id), 'package.vmod');
|
|
}
|
|
|
|
list() {
|
|
if (!existsSync(this.#root)) return [];
|
|
const result = [];
|
|
for (const entry of readdirSync(this.#root, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) continue;
|
|
try {
|
|
const packagePath = this.#packagePath(entry.name);
|
|
if (!existsSync(packagePath)) continue;
|
|
const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
const pkg = validateModulePackage(parsed, { trustKeys: this.#trustKeys });
|
|
result.push({
|
|
id: pkg.manifest.id,
|
|
manifest: pkg.manifest,
|
|
runtime: pkg.runtime,
|
|
trust: pkg.trust,
|
|
installedAt: String(parsed.installedAt || parsed.createdAt || ''),
|
|
activatable: pkg.runtime.mode === 'declarative' || (pkg.runtime.mode === 'isolated' && pkg.trust.trusted),
|
|
packagePath,
|
|
});
|
|
} catch (error) {
|
|
result.push({ id: entry.name, invalid: true, error: error?.message || String(error), activatable: false });
|
|
}
|
|
}
|
|
return result.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
}
|
|
|
|
get(id) {
|
|
const packagePath = this.#packagePath(id);
|
|
if (!existsSync(packagePath)) return null;
|
|
const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
return validateModulePackage(parsed, { trustKeys: this.#trustKeys });
|
|
}
|
|
|
|
install(input) {
|
|
const pkg = validateModulePackage(input, { trustKeys: this.#trustKeys });
|
|
const root = this.#moduleRoot(pkg.manifest.id);
|
|
if (existsSync(root)) throw new PlatformError('Dieses Fremdmodul ist bereits installiert.', { code: 'MODULE_ALREADY_INSTALLED', status: 409, expose: true });
|
|
const temporary = `${root}.${process.pid}.tmp`;
|
|
mkdirSync(temporary, { recursive: true });
|
|
try {
|
|
for (const file of pkg.files) {
|
|
const destination = ensureInside(temporary, join(temporary, file.path));
|
|
mkdirSync(dirname(destination), { recursive: true });
|
|
writeFileSync(destination, Buffer.from(file.content, 'base64'), { mode: 0o600 });
|
|
}
|
|
const persisted = { ...pkg, trust: undefined, installedAt: new Date().toISOString() };
|
|
writeFileSync(join(temporary, 'package.vmod'), `${JSON.stringify(persisted, null, 2)}\n`, { mode: 0o600 });
|
|
renameSync(temporary, root);
|
|
} catch (error) {
|
|
rmSync(temporary, { recursive: true, force: true });
|
|
throw error;
|
|
}
|
|
return this.describe(pkg.manifest.id);
|
|
}
|
|
|
|
remove(id) {
|
|
const root = this.#moduleRoot(id);
|
|
if (!existsSync(root)) return false;
|
|
rmSync(root, { recursive: true, force: true });
|
|
return true;
|
|
}
|
|
|
|
export(id) {
|
|
const packagePath = this.#packagePath(id);
|
|
if (!existsSync(packagePath)) throw new PlatformError('Installiertes Modulpaket nicht gefunden.', { code: 'MODULE_NOT_FOUND', status: 404, expose: true });
|
|
return readFileSync(packagePath);
|
|
}
|
|
|
|
describe(id) {
|
|
const pkg = this.get(id);
|
|
if (!pkg) return null;
|
|
return {
|
|
id: pkg.manifest.id,
|
|
manifest: pkg.manifest,
|
|
runtime: pkg.runtime,
|
|
trust: pkg.trust,
|
|
activatable: pkg.runtime.mode === 'declarative' || (pkg.runtime.mode === 'isolated' && pkg.trust.trusted),
|
|
};
|
|
}
|
|
}
|