[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
194
electron/plugins/permissionResources.cjs
Normal file
194
electron/plugins/permissionResources.cjs
Normal file
@@ -0,0 +1,194 @@
|
||||
"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,
|
||||
};
|
||||
Reference in New Issue
Block a user