Files
NetMesh/electron/plugins/credentialBroker.cjs

130 lines
4.7 KiB
JavaScript
Raw Normal View History

"use strict";
const { PluginRpcError, RPC_ERRORS } = require("./rpcRouter.cjs");
const { MAX_SECRET_BYTES, assertSecretRef } = require("./secretStore.cjs");
function assertCredentialRef(credential) {
if (credential?.kind === "secret") {
return { kind: "secret", ...assertSecretRef(credential) };
}
if (
!credential
|| typeof credential !== "object"
|| Array.isArray(credential)
|| credential.kind !== "credential"
|| typeof credential.id !== "string"
|| credential.id.length < 16
|| credential.id.length > 256
) throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Credential reference is invalid");
return { kind: "credential", id: credential.id };
}
function assertLeaseParams(params) {
if (!params || typeof params !== "object" || Array.isArray(params)) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Credential lease parameters are invalid");
}
const credential = assertCredentialRef(params.secret);
if (typeof params.operationId !== "string" || params.operationId.length < 1 || params.operationId.length > 128) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Credential lease operation ID is invalid");
}
if (typeof params.purpose !== "string" || params.purpose.length < 1 || params.purpose.length > 256) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Credential lease purpose is invalid");
}
if (
params.ttlMs !== undefined
&& (!Number.isSafeInteger(params.ttlMs) || params.ttlMs < 1 || params.ttlMs > 60_000)
) throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Credential lease lifetime is invalid");
return {
secret: credential,
operationId: params.operationId,
purpose: params.purpose,
...(params.ttlMs === undefined ? {} : { ttlMs: params.ttlMs }),
};
}
class PluginCredentialBroker {
constructor(options) {
this.secretStore = options.secretStore;
this.leaseStore = options.leaseStore;
this.credentialResolver = options.credentialResolver ?? null;
if (this.credentialResolver && (
typeof this.credentialResolver.assertReference !== "function"
|| typeof this.credentialResolver.resolve !== "function"
)) throw new TypeError("Credential resolver must validate and resolve credential references");
}
describeAuthorization(params) {
const validated = assertLeaseParams(params);
if (validated.secret.kind === "credential") {
return {
permission: "vault.credentials",
resources: [`credential:${validated.secret.id}`],
reason: `Use Netcatty credential for ${validated.purpose}`,
operationId: validated.operationId,
};
}
return {
permission: "secrets",
resources: [`secret:${validated.secret.key}`],
reason: `Use plugin secret for ${validated.purpose}`,
operationId: validated.operationId,
};
}
async createLease(params, context) {
const validated = assertLeaseParams(params);
await context.assertActive();
let resolveSecret;
if (validated.secret.kind === "credential") {
if (!this.credentialResolver) {
throw new PluginRpcError(RPC_ERRORS.unavailable, "Netcatty credential access is unavailable");
}
await this.credentialResolver.assertReference(validated.secret, {
pluginId: context.pluginId,
runtimeId: context.runtimeId,
operationId: validated.operationId,
purpose: validated.purpose,
signal: context.signal,
});
await context.assertActive();
resolveSecret = async (consumeContext) => {
const value = await this.credentialResolver.resolve(
validated.secret,
Object.freeze({ ...consumeContext, purpose: validated.purpose }),
);
if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > MAX_SECRET_BYTES) {
throw new PluginRpcError(RPC_ERRORS.dataLoss, "Resolved credential is invalid or too large");
}
return value;
};
} else {
this.secretStore.getRecordByReference(context.pluginId, validated.secret);
}
return this.leaseStore.issue({
pluginId: context.pluginId,
runtimeId: context.runtimeId,
credential: validated.secret,
operationId: validated.operationId,
purpose: validated.purpose,
ttlMs: validated.ttlMs,
signal: context.signal,
resolveSecret,
});
}
async consumeLease(context, lease, operationId) {
await context.assertActive();
const value = await this.leaseStore.consume({
pluginId: context.pluginId,
runtimeId: context.runtimeId,
lease,
operationId,
signal: context.signal,
});
await context.assertActive();
return value;
}
}
module.exports = { PluginCredentialBroker, assertCredentialRef, assertLeaseParams };