Files
NetMesh/electron/plugins/permissionResources.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

195 lines
6.8 KiB
JavaScript

"use strict";
const { createHash } = require("node:crypto");
const path = require("node:path");
const contractSchema = require("./generated/plugin-contract.schema.json");
const PLUGIN_PERMISSIONS = new Set(contractSchema.$defs.PluginPermission.enum);
const RESOURCE_SCOPED_PERMISSIONS = new Set(
contractSchema.$defs.ResourceScopedPermission.enum,
);
function assertPluginPermission(permission) {
if (!PLUGIN_PERMISSIONS.has(permission)) {
throw new TypeError(`Unknown plugin permission: ${String(permission)}`);
}
return permission;
}
function canonicalizeNetworkOrigin(value) {
if (typeof value !== "string" || value.length < 1 || value.length > 2_048) {
throw new TypeError("Plugin network origin is invalid");
}
let parsed;
try { parsed = new URL(value); }
catch { throw new TypeError("Plugin network origin is invalid"); }
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
throw new TypeError("Plugin network access supports only HTTP and HTTPS origins");
}
if (
parsed.username
|| parsed.password
|| (parsed.pathname !== "" && parsed.pathname !== "/")
|| parsed.search
|| parsed.hash
) throw new TypeError("Plugin network grants must contain an origin without credentials or a path");
return parsed.origin;
}
function canonicalizeCompanionResource(value) {
if (
typeof value !== "string"
|| value.length < 5
|| value.length > 192
|| !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+\.[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)*$/u.test(value)
) throw new TypeError("Plugin companion resource is invalid");
return value;
}
function canonicalizeFilesystemResource(value) {
if (
typeof value !== "string"
|| value.length < 1
|| value.length > 8_192
|| value.includes("\0")
|| !path.isAbsolute(value)
) {
throw new TypeError("Plugin filesystem grants require an absolute canonical path");
}
return path.normalize(value);
}
function canonicalizePermissionResource(permission, resource) {
assertPluginPermission(permission);
if (resource === "*") return resource;
if (permission === "network") return canonicalizeNetworkOrigin(resource);
if (permission === "companion.execute") return canonicalizeCompanionResource(resource);
if (permission === "filesystem.read" || permission === "filesystem.write") {
return canonicalizeFilesystemResource(resource);
}
if (typeof resource !== "string" || resource.length < 1 || resource.length > 2_048) {
throw new TypeError("Plugin permission resource is invalid");
}
return resource;
}
function normalizeForFilesystemComparison(value) {
const normalized = path.resolve(value);
return process.platform === "win32" ? normalized.toLocaleLowerCase("en-US") : normalized;
}
function permissionResourceCovers(
permission,
grantResource,
requestedResource,
resourceKind = "exact",
requestedResourceKind = "exact",
) {
if (grantResource === "*") return true;
if (permission === "filesystem.read" || permission === "filesystem.write") {
if (requestedResourceKind === "directory" && resourceKind !== "directory") return false;
const parent = normalizeForFilesystemComparison(grantResource);
const candidate = normalizeForFilesystemComparison(requestedResource);
if (resourceKind !== "directory") return parent === candidate;
const relative = path.relative(parent, candidate);
return relative === ""
|| (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
}
return grantResource === requestedResource;
}
function normalizePermissionDeclarations(manifest) {
const declarations = new Map();
for (const [required, values] of [
[true, manifest?.permissions?.required ?? []],
[false, manifest?.permissions?.optional ?? []],
]) {
for (const value of values) {
const permission = assertPluginPermission(typeof value === "string" ? value : value.permission);
const resources = typeof value === "string"
? []
: [...new Set((value.resources ?? []).map((resource) => (
canonicalizePermissionResource(permission, resource)
)))].sort();
if (required && RESOURCE_SCOPED_PERMISSIONS.has(permission)) {
if (resources.length === 0) {
throw new TypeError(`Required permission ${permission} must declare resources`);
}
if (resources.includes("*")) {
throw new TypeError(`Required permission ${permission} must not use wildcard resources`);
}
}
declarations.set(permission, Object.freeze({
permission,
required,
resources: Object.freeze(resources),
reason: typeof value === "string" ? "" : value.reason ?? "",
}));
}
}
return declarations;
}
function defaultSecurityPrincipal(manifest, archiveSha256) {
if (!manifest || typeof manifest.id !== "string" || typeof manifest.publisher !== "string") {
throw new TypeError("Plugin security principal requires manifest identity");
}
if (
archiveSha256 !== undefined
&& (typeof archiveSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(archiveSha256))
) throw new TypeError("Plugin security principal package digest is invalid");
const digest = createHash("sha256")
.update(manifest.id)
.update("\0")
.update(manifest.publisher)
.update("\0")
.update(archiveSha256 ?? "unbound-development-package")
.digest("hex");
return `${archiveSha256 ? "unsigned-package" : "unsigned-development"}:${digest}`;
}
function assertSecurityPrincipal(value) {
if (typeof value !== "string" || value.length < 16 || value.length > 256 || value.includes("\0")) {
throw new TypeError("Plugin security principal is invalid");
}
return value;
}
function permissionDeclarationHash(declaration, securityPrincipal = "legacy:unbound-principal") {
return createHash("sha256").update(JSON.stringify({
permission: declaration.permission,
required: declaration.required,
resources: [...declaration.resources].sort(),
securityPrincipal: assertSecurityPrincipal(securityPrincipal),
})).digest("hex");
}
function declarationAllowsResource(declaration, requestedResource) {
if (declaration.resources.length === 0) return true;
return declaration.resources.some((resource) => permissionResourceCovers(
declaration.permission,
resource,
requestedResource,
declaration.permission === "filesystem.read" || declaration.permission === "filesystem.write"
? "directory"
: "exact",
));
}
module.exports = {
PLUGIN_PERMISSIONS,
RESOURCE_SCOPED_PERMISSIONS,
assertPluginPermission,
assertSecurityPrincipal,
canonicalizeCompanionResource,
canonicalizeFilesystemResource,
canonicalizeNetworkOrigin,
canonicalizePermissionResource,
declarationAllowsResource,
defaultSecurityPrincipal,
normalizePermissionDeclarations,
permissionDeclarationHash,
permissionResourceCovers,
};