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
163 lines
5.4 KiB
JavaScript
163 lines
5.4 KiB
JavaScript
"use strict";
|
|
|
|
const { randomBytes } = require("node:crypto");
|
|
|
|
const { PluginRpcError, RPC_ERRORS } = require("./rpcRouter.cjs");
|
|
|
|
const DEFAULT_SECRET_LEASE_TTL_MS = 30_000;
|
|
const MAX_SECRET_LEASE_TTL_MS = 60_000;
|
|
const MAX_SECRET_LEASES_PER_RUNTIME = 128;
|
|
|
|
function assertLeaseReference(lease) {
|
|
if (
|
|
!lease
|
|
|| typeof lease !== "object"
|
|
|| Array.isArray(lease)
|
|
|| lease.kind !== "secret-lease"
|
|
|| typeof lease.id !== "string"
|
|
|| lease.id.length < 32
|
|
|| lease.id.length > 256
|
|
) throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Secret lease reference is invalid");
|
|
return lease.id;
|
|
}
|
|
|
|
class SecretLeaseStore {
|
|
constructor(options) {
|
|
this.secretStore = options.secretStore;
|
|
this.clock = options.clock ?? (() => Date.now());
|
|
this.randomBytes = options.randomBytes ?? randomBytes;
|
|
this.setTimer = options.setTimeout ?? setTimeout;
|
|
this.clearTimer = options.clearTimeout ?? clearTimeout;
|
|
this.leases = new Map();
|
|
this.closed = false;
|
|
}
|
|
|
|
issue(options) {
|
|
if (this.closed) throw new PluginRpcError(RPC_ERRORS.unavailable, "Secret lease broker is closed");
|
|
if (options.signal?.aborted) {
|
|
throw new PluginRpcError(RPC_ERRORS.cancelled, "Secret lease operation was cancelled");
|
|
}
|
|
const activeCount = [...this.leases.values()].filter((lease) => (
|
|
lease.runtimeId === options.runtimeId
|
|
)).length;
|
|
if (activeCount >= MAX_SECRET_LEASES_PER_RUNTIME) {
|
|
throw new PluginRpcError(RPC_ERRORS.resourceExhausted, "Too many active secret leases");
|
|
}
|
|
const ttlMs = options.ttlMs ?? DEFAULT_SECRET_LEASE_TTL_MS;
|
|
if (!Number.isSafeInteger(ttlMs) || ttlMs < 1 || ttlMs > MAX_SECRET_LEASE_TTL_MS) {
|
|
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Secret lease lifetime is invalid");
|
|
}
|
|
const id = this.randomBytes(24).toString("base64url");
|
|
if (options.resolveSecret !== undefined && typeof options.resolveSecret !== "function") {
|
|
throw new TypeError("Secret lease resolver must be a function");
|
|
}
|
|
const credential = options.credential ?? options.secret;
|
|
if (!credential) throw new TypeError("Secret lease credential is required");
|
|
const expiresAt = this.clock() + ttlMs;
|
|
const record = {
|
|
id,
|
|
pluginId: options.pluginId,
|
|
runtimeId: options.runtimeId,
|
|
credential,
|
|
resolveSecret: options.resolveSecret ?? null,
|
|
operationId: options.operationId,
|
|
purpose: options.purpose,
|
|
expiresAt,
|
|
timer: null,
|
|
abortCleanup: null,
|
|
};
|
|
record.timer = this.setTimer(() => this.#delete(id), ttlMs);
|
|
record.timer?.unref?.();
|
|
if (options.signal) {
|
|
const onAbort = () => this.#delete(id);
|
|
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
record.abortCleanup = () => options.signal.removeEventListener("abort", onAbort);
|
|
}
|
|
this.leases.set(id, record);
|
|
if (options.signal?.aborted) {
|
|
this.#delete(id);
|
|
throw new PluginRpcError(RPC_ERRORS.cancelled, "Secret lease operation was cancelled");
|
|
}
|
|
return Object.freeze({
|
|
kind: "secret-lease",
|
|
id,
|
|
operationId: options.operationId,
|
|
expiresAt,
|
|
});
|
|
}
|
|
|
|
consume(options) {
|
|
if (options.signal?.aborted) {
|
|
throw new PluginRpcError(RPC_ERRORS.cancelled, "Secret lease operation was cancelled");
|
|
}
|
|
const id = assertLeaseReference(options.lease);
|
|
const record = this.leases.get(id);
|
|
if (!record || record.expiresAt <= this.clock()) {
|
|
this.#delete(id);
|
|
throw new PluginRpcError(RPC_ERRORS.notFound, "Secret lease is missing or expired");
|
|
}
|
|
if (
|
|
record.pluginId !== options.pluginId
|
|
|| record.runtimeId !== options.runtimeId
|
|
|| record.operationId !== options.operationId
|
|
) throw new PluginRpcError(RPC_ERRORS.permissionDenied, "Secret lease scope does not match the operation");
|
|
this.#delete(id);
|
|
const consumeContext = Object.freeze({
|
|
pluginId: record.pluginId,
|
|
runtimeId: record.runtimeId,
|
|
operationId: record.operationId,
|
|
credential: record.credential,
|
|
signal: options.signal,
|
|
});
|
|
const resolved = record.resolveSecret
|
|
? record.resolveSecret(consumeContext)
|
|
: this.secretStore.resolve(record.pluginId, record.credential);
|
|
if (!resolved || typeof resolved.then !== "function") {
|
|
if (options.signal?.aborted) {
|
|
throw new PluginRpcError(RPC_ERRORS.cancelled, "Secret lease operation was cancelled");
|
|
}
|
|
return resolved;
|
|
}
|
|
return Promise.resolve(resolved).then((value) => {
|
|
if (options.signal?.aborted) {
|
|
throw new PluginRpcError(RPC_ERRORS.cancelled, "Secret lease operation was cancelled");
|
|
}
|
|
return value;
|
|
});
|
|
}
|
|
|
|
#delete(id) {
|
|
const record = this.leases.get(id);
|
|
if (!record) return false;
|
|
this.leases.delete(id);
|
|
this.clearTimer(record.timer);
|
|
record.abortCleanup?.();
|
|
return true;
|
|
}
|
|
|
|
revokeRuntime(runtimeId) {
|
|
for (const [id, lease] of [...this.leases]) {
|
|
if (lease.runtimeId === runtimeId) this.#delete(id);
|
|
}
|
|
}
|
|
|
|
revokeOperation(pluginId, operationId) {
|
|
for (const [id, lease] of [...this.leases]) {
|
|
if (lease.pluginId === pluginId && lease.operationId === operationId) this.#delete(id);
|
|
}
|
|
}
|
|
|
|
shutdown() {
|
|
this.closed = true;
|
|
for (const id of [...this.leases.keys()]) this.#delete(id);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
DEFAULT_SECRET_LEASE_TTL_MS,
|
|
MAX_SECRET_LEASES_PER_RUNTIME,
|
|
MAX_SECRET_LEASE_TTL_MS,
|
|
SecretLeaseStore,
|
|
assertLeaseReference,
|
|
};
|