openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
733 lines (732 loc) • 32.4 kB
JavaScript
import { r as getAsyncWorkSignal } from "./async-work-scope-BYeX3gh6.mjs";
import { s as registerSecretValueForRedaction } from "./secret-redaction-registry-b2B9lcfy.mjs";
import { t as assertExistingDatabaseIdentity } from "./sqlite-worker-identity-B_aK_kcL.mjs";
import { t as isPromiseLike } from "./promise-like-D7-l5Fsp.mjs";
import { r as isIncognitoSessionKey } from "./session-key-DDIJxXfQ.mjs";
import "./session-key-BC4m_Ly5.mjs";
import { n as sessionChanges } from "./session-row-changes-ZcHZZvDt.mjs";
import { Ar as validateQuestionWaitAnswerParams, Dr as validateQuestionListParams, Er as validateQuestionGetParams, Or as validateQuestionRequestParams, kr as validateQuestionResolveParams } from "./src-D2gvuSyP.mjs";
import { n as SecretStoreValidationError } from "./secret-store-validation-error-Bzzf_1MN.mjs";
import { t as assertAdmittedRunOperatorAuthority } from "./admitted-run-context-CbCp9bj_.mjs";
import { l as readUserProfileAliases } from "./user-profile-list-y8HW3dT5.mjs";
import { t as ErrorCodes } from "./gateway-error-details-DIefe7Zy.mjs";
import { f as errorShape } from "./error-codes-DI20vDEB.mjs";
import { n as authorizeGatewaySessionCreation, o as operatorSessionCap, r as hasOperatorBoundary, t as authorizeCurrentOperatorRoleScopes, u as resolveGatewayOperatorRoleActor, v as readGatewayAccessRevision } from "./operator-role-policy-C96xz3qI.mjs";
import { n as captureGatewayOperatorRunAuthority } from "./operator-run-authority-CPSb2K99.mjs";
import { r as listSecretStoreEntries } from "./secret-store-CntTyjmN.mjs";
import { D as resolveSessionStorePathForScope } from "./session-accessor.sqlite-entry-BuCmtpuj.mjs";
import { n as resolveRequestedSessionAgentId } from "./session-request-agent-D8Xj-iBG.mjs";
import { a as resolveStoredSessionKeyForAgentStore } from "./session-store-key-11PJMhso.mjs";
import { i as retainSessionHistoryWorkerDatabase } from "./session-transcript-worker-runtime-BPDJyRf8.mjs";
import { o as withSessionEntriesFromStoresInWorker } from "./session-entry-read-runtime-D37tIemF.mjs";
import { h as registerActiveEmbeddedRunHumanInputWait } from "./run-state-BCpgti2M.mjs";
import { n as handleQuestionChannelResolved, t as handleQuestionChannelRequested } from "./question-channel-runtime-j3i1l3mj.mjs";
import { t as questionShapeError } from "./question-validation-C3R346Mf.mjs";
import { a as readGatewayRequestMutationAuthority } from "./session-mutation-guards-C7E56jUW.mjs";
import { f as isGatewayAdmin, r as authorizeOwnSessionMutation, y as sharingIdentity } from "./session-sharing-policy-QAgS-DcQ.mjs";
import { S as prepareSessionSharing, v as canReceiveSessionEvent } from "./session-sharing-KNQSWoOJ.mjs";
import { t as assertValidParams } from "./validation-B7TobU6-.mjs";
import { n as usesOwnRunQuestionAccess, t as canSelectQuestion } from "./question-access-Zri0o4CD.mjs";
import { n as QuestionManagerError, r as QuestionManagerErrorCodes } from "./question-manager-BIP4Oi7t.mjs";
//#region src/gateway/question-session-access.ts
function prepareQuestionSharing(cfg, client, isMember) {
const identity = sharingIdentity(client, resolveGatewayOperatorRoleActor(client));
return prepareSessionSharing({
cfg,
client
}, {
aliases: identity ? readUserProfileAliases(identity.id) : /* @__PURE__ */ new Set(),
sessionCap: operatorSessionCap(client, cfg),
isMember
});
}
/** Batch a response's targets; no row or recipient predicate remains authoritative after consume. */
async function withPreparedQuestionSessions(options, questions, consume, operation) {
const signal = getAsyncWorkSignal();
while (true) {
operation.assertCurrent();
const cfg = options.context.getRuntimeConfig();
const accessRevision = readGatewayAccessRevision();
let changed = false;
const groups = /* @__PURE__ */ new Map();
const selections = questions.map((question) => {
if (!question.sessionKey) return;
const resolved = resolveRequestedSessionAgentId(cfg, question.sessionKey, question.agentId);
if (!resolved.ok) return;
const agentId = resolved.agentId;
const sessionKey = resolveStoredSessionKeyForAgentStore({
cfg,
agentId,
sessionKey: question.sessionKey
});
const storePath = resolveSessionStorePathForScope({
agentId,
sessionKey
}, cfg);
const key = JSON.stringify([agentId, storePath]);
const group = groups.get(key) ?? {
agentId,
storePath,
sessionKeys: [],
includeMembers: operation.includeMembers ?? false,
includeAuthorization: true
};
group.sessionKeys.push(sessionKey);
groups.set(key, group);
return {
key,
agentId,
sessionKey,
storePath,
binding: question.sessionAccess
};
});
const readSignal = groups.size > 0 ? signal : void 0;
readSignal?.throwIfAborted();
const keys = [...groups.keys()];
const unsubscribe = sessionChanges.subscribe((change) => {
if ("all" in change) {
const scope = change.scope;
changed ||= typeof scope === "string" || selections.some((selected) => selected && (!scope.agentId || selected.agentId === scope.agentId) && (!scope.storePath || selected.storePath === scope.storePath || Boolean(scope.agentId)));
} else changed ||= selections.some((selected) => selected && (!change.agentId || selected.agentId === change.agentId) && selected.sessionKey === change.sessionKey);
});
try {
const outcome = await withSessionEntriesFromStoresInWorker([...groups.values()], (reads) => {
readSignal?.throwIfAborted();
operation.assertCurrent();
if (changed || accessRevision !== readGatewayAccessRevision() || cfg !== options.context.getRuntimeConfig()) return { retry: true };
let active = true;
try {
const value = consume(selections.map((selection) => {
if (!selection) return;
const read = reads[keys.indexOf(selection.key)];
const entry = read.result.entries.find((row) => row.sessionKey === selection.sessionKey)?.entry;
const target = entry ? {
agentId: selection.agentId,
canonicalKey: selection.sessionKey,
storePath: selection.storePath,
storeKey: selection.sessionKey,
storeKeys: [selection.sessionKey],
entry
} : null;
const assertCurrent = () => {
if (!active || changed || cfg !== options.context.getRuntimeConfig() || accessRevision !== readGatewayAccessRevision()) throw new Error("Question session preparation is no longer current");
read.assertCurrent();
const identity = read.result.databaseIdentity;
if (identity) assertExistingDatabaseIdentity(read.database.path, `file:${identity.identity}`);
const currentCfg = options.context.getRuntimeConfig();
if (resolveSessionStorePathForScope({
agentId: selection.agentId,
sessionKey: selection.sessionKey
}, currentCfg) !== selection.storePath || resolveStoredSessionKeyForAgentStore({
cfg: currentCfg,
...selection
}) !== selection.sessionKey) throw new Error("Question session route changed");
};
const recipients = /* @__PURE__ */ new Map();
const sharingFor = (client) => {
if (client?.invalidated) throw new Error("Question recipient is no longer current");
client?.internal?.operatorAccessAuthority?.assertCurrent();
client?.internal?.operatorRunAuthority?.assertCurrent();
let sharing = recipients.get(client);
if (!sharing) {
sharing = prepareQuestionSharing(cfg, client, (selected, identityId) => read.result.members?.[selected.canonicalKey]?.some((member) => member.identityId === identityId) ?? false);
recipients.set(client, sharing);
}
return sharing;
};
const preparedSession = {
target,
read,
assertCurrent,
canAccess: (client, _access, narrow, binding = selection.binding) => {
try {
if (narrow && binding) binding.assertCurrent(preparedSession);
else assertCurrent();
if (!target) return false;
if (narrow) {
client?.internal?.operatorAccessAuthority?.assertCurrent();
client?.internal?.operatorRunAuthority?.assertCurrent();
if (!binding?.canSelect(client) || authorizeCurrentOperatorRoleScopes(client, cfg) || target.entry.incognito || isIncognitoSessionKey(target.canonicalKey)) return false;
const actor = resolveGatewayOperatorRoleActor(client);
return actor?.kind === "operator" && !authorizeOwnSessionMutation({
client,
target,
expectedProfileId: actor.profileId
});
}
return sharingFor(client).entryFilter?.(target.canonicalKey, target.entry) ?? true;
} catch {
return false;
}
},
authorizeMutation: (client) => {
assertCurrent();
return target ? sharingFor(client).authorizeTarget(target) : null;
},
canReceive: (client) => {
try {
assertCurrent();
return canReceiveSessionEvent({
cfg,
client,
sessionKeys: [selection.sessionKey],
agentId: selection.agentId,
prepared: {
sharing: sharingFor(client),
target: () => target
}
});
} catch {
return false;
}
}
};
return preparedSession;
}));
if (isPromiseLike(value)) {
Promise.resolve(value).catch(() => {});
throw new Error("Question session consumers must remain synchronous");
}
return {
retry: false,
value
};
} finally {
active = false;
}
});
if (!outcome.retry) return outcome.value;
} finally {
unsubscribe();
}
}
}
/** Capture before the first worker await, then bind only the exact admitted row in its consumer. */
async function withQuestionSessionAccess(options, sessionKey, agentId, consume, operation) {
operation.assertCurrent();
const producer = resolveGatewayOperatorRoleActor(options.client);
const source = await captureGatewayOperatorRunAuthority(options);
const profileId = usesOwnRunQuestionAccess(options.client) && options.client?.internal?.operatorRunAuthority ? source?.authority.profileId : void 0;
let transferred = false;
try {
return await withPreparedQuestionSessions(options, [{
sessionKey,
agentId
}], ([prepared]) => {
const selected = prepared?.target;
if (!prepared || !selected?.entry.sessionId || !selected.entry.lifecycleRevision || selected.entry.incognito || isIncognitoSessionKey(selected.canonicalKey)) return consume(void 0, prepared);
source?.authority.assertCurrent();
const retained = retainSessionHistoryWorkerDatabase(prepared.read.database);
const identity = prepared.read.result.databaseIdentity;
const original = {
agentId: selected.agentId,
sessionKey: selected.canonicalKey,
storePath: selected.storePath,
databasePath: prepared.read.database.path,
sessionId: selected.entry.sessionId,
lifecycleRevision: selected.entry.lifecycleRevision
};
let released = false;
let invalidated = false;
const assertSourceCurrent = () => {
if (released || invalidated) throw new Error("Question session source was released");
retained.owner.assertCurrent();
source?.authority.assertCurrent();
const currentProducer = resolveGatewayOperatorRoleActor(options.client);
if (producer?.kind === "operator" && (currentProducer?.kind !== "operator" || producer.profileId !== currentProducer.profileId)) throw new Error("Question producer identity changed");
};
const access = {
agentId: original.agentId,
sessionKey: original.sessionKey,
canSelect: (client) => Boolean(profileId && client && !client.invalidated && (client.connect.role ?? "operator") === "operator" && !authorizeOwnSessionMutation({
client,
target: null,
expectedProfileId: profileId
})),
assertSourceCurrent,
assertCurrent: (current) => {
assertSourceCurrent();
current.assertCurrent();
const next = current.target;
const nextIdentity = current.read.result.databaseIdentity;
if (!identity || !nextIdentity || identity.identity !== nextIdentity.identity || identity.birthtime !== nextIdentity.birthtime || current.read.database.path !== original.databasePath || next?.agentId !== original.agentId || next.canonicalKey !== original.sessionKey || next.storePath !== original.storePath || next.entry.sessionId !== original.sessionId || next.entry.lifecycleRevision !== original.lifecycleRevision || next.entry.incognito) {
invalidated = true;
throw new Error("Question session generation changed");
}
},
release: () => {
if (!released) {
released = true;
try {
retained.release();
} finally {
source?.release();
}
}
}
};
transferred = true;
try {
const value = consume(access, prepared);
if (isPromiseLike(value)) {
Promise.resolve(value).catch(() => {});
throw new Error("Question session consumers must remain synchronous");
}
return value;
} catch (error) {
access.release();
throw error;
}
}, {
assertCurrent: () => {
operation.assertCurrent();
source?.authority.assertCurrent();
},
includeMembers: operation.includeMembers
});
} finally {
if (!transferred) source?.release();
}
}
function canAccessSessionQuestion(observation, prepared, client, access) {
try {
if (!observation?.isCurrent() || !observation.ordinary || !observation.sessionAccess?.canSelect(client) || !prepared) return false;
const allowed = prepared.canAccess(client, access, true, observation.sessionAccess);
if (!allowed) observation.refreshRequester();
return allowed;
} catch {
return false;
}
}
function questionNotFound(id) {
return errorShape(ErrorCodes.INVALID_REQUEST, `question '${id}' was not found`, { details: { reason: QuestionManagerErrorCodes.NOT_FOUND } });
}
function prepareQuestionAuthorization(options, observation, id, access) {
const authority = readGatewayRequestMutationAuthority(options);
const actor = resolveGatewayOperatorRoleActor(options.client);
const narrow = usesOwnRunQuestionAccess(options.client);
return {
target: narrow || !isGatewayAdmin(options.client) && hasOperatorBoundary(options.client, options.context.getRuntimeConfig()) ? {
...observation?.record,
sessionAccess: observation?.sessionAccess
} : {},
assertCurrent: () => {
authority.assertCurrent();
if (narrow && observation?.sessionAccess) try {
observation.sessionAccess.assertSourceCurrent();
} catch {
observation.refreshRequester();
throw new QuestionManagerError(QuestionManagerErrorCodes.NOT_FOUND, `question '${id}' was not found`);
}
},
authorize: (prepared) => {
if (!observation?.isCurrent()) return questionNotFound(id);
if (narrow) {
authority.assertCurrent();
const current = resolveGatewayOperatorRoleActor(options.client);
if (actor?.kind !== "operator" || current?.kind !== "operator" || current.profileId !== actor.profileId || !canAccessSessionQuestion(observation, prepared, options.client, access)) return questionNotFound(id);
return null;
}
if (isGatewayAdmin(options.client) || !hasOperatorBoundary(options.client, options.context.getRuntimeConfig()) || !observation.record.sessionKey) return null;
if (!prepared?.canAccess(options.client, "read", false)) return questionNotFound(id);
return access === "mutate" ? prepared.authorizeMutation(options.client) : null;
}
};
}
function questionBroadcastOptions(params) {
const { observation, prepared, expectedRecord, cfg, isPublishing } = params;
const sessionKey = observation?.record.sessionKey;
if (!prepared && !sessionKey) return;
const isCurrent = () => Boolean(isPublishing() && observation?.isCurrent() && (!expectedRecord || observation.record === expectedRecord && expectedRecord.status === "pending"));
return { questionRecipient: (client) => {
if (!isCurrent()) return false;
if (usesOwnRunQuestionAccess(client)) return canAccessSessionQuestion(observation, prepared, client, "read");
if (prepared) return prepared.canReceive(client);
return canReceiveSessionEvent({
cfg,
client,
sessionKeys: sessionKey ? [sessionKey] : [],
agentId: observation?.record.agentId,
prepared: {
sharing: prepareQuestionSharing(cfg, client, () => false),
target: () => null
}
});
} };
}
//#endregion
//#region src/gateway/server-methods/question.ts
const DEFAULT_QUESTION_TIMEOUT_MS = 9e5;
var QuestionRequestValidationError = class extends Error {};
function managerError(error, respond) {
if (!(error instanceof QuestionManagerError)) return false;
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message, { details: { reason: error.code } }));
return true;
}
function normalizeQuestions(params) {
const error = questionShapeError(params.questions, {
allowPlainSecretQuestions: false,
validateUrls: true
});
if (error) throw new QuestionRequestValidationError(error);
return params.questions.map((question) => {
const binding = question.secretStore;
if (binding) {
const existing = listSecretStoreEntries({ scope: { kind: "team" } }).find((entry) => entry.name === binding.name);
return {
...question,
secretStore: {
...binding,
allowedHosts: binding.allowedHosts ?? existing?.allowedHosts ?? []
},
...existing ? { secretStoreExisting: {
updatedAtMs: existing.updatedAtMs,
...existing.updatedBy ? { updatedBy: existing.updatedBy } : {}
} } : {}
};
}
return question;
});
}
/** Creates the lazily loaded question RPC surface for one Gateway lifetime. */
function createQuestionHandlers(manager, storeWriteService, scheduler) {
const prepareSelectedQuestion = (options, id, access) => {
readGatewayRequestMutationAuthority(options).assertCurrent();
const question = canSelectQuestion(manager, id, options.client) ? manager.get(id) : null;
if (!question) {
options.respond(false, void 0, questionNotFound(id));
return;
}
const observation = manager.observe(id, question);
return {
question,
observation,
authorize: prepareQuestionAuthorization(options, observation, id, access)
};
};
return {
"question.request": async (options) => {
const { params, respond, context, client } = options;
if (!assertValidParams(params, validateQuestionRequestParams, "question.request", respond)) return;
let request = params;
const storeBound = request.questions.some((question) => question.secretStore);
const authority = readGatewayRequestMutationAuthority(options);
authority.assertCurrent();
const narrow = usesOwnRunQuestionAccess(client);
let sessionAccess;
let accepted = false;
const requiresSharing = () => !isGatewayAdmin(client) && hasOperatorBoundary(client, context.getRuntimeConfig());
if (storeBound && !isGatewayAdmin(client)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "secret store questions require an operator.admin client"));
return;
}
const identity = client?.internal?.agentRuntimeIdentity;
const validateAuthority = context.validateAgentRuntimeApprovalAuthority;
if ((storeBound || narrow) && (!identity || !validateAuthority)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, storeBound ? "secret store questions require trusted agent runtime authority" : "question creation requires trusted agent runtime authority"));
return;
}
const requester = identity ? structuredClone(identity) : void 0;
const operatorAuthority = client?.internal?.operatorRunAuthority;
const isRequesterActive = requester && validateAuthority ? () => {
try {
operatorAuthority?.assertCurrent();
sessionAccess?.assertSourceCurrent();
return validateAuthority(requester);
} catch {
return false;
}
} : void 0;
if (narrow && (!operatorAuthority || storeBound || request.questions.some((question) => question.isSecret) || isRequesterActive?.() !== true)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Session-scoped questions require an ordinary question and a live agent requester."));
return;
}
if (requester) request = {
...request,
agentId: requester.agentId,
sessionKey: requester.sessionKey,
runId: requester.operationalRunInstance.runId
};
try {
if (narrow && operatorAuthority) assertAdmittedRunOperatorAuthority(operatorAuthority);
const requestedSession = request.sessionKey ? resolveRequestedSessionAgentId(context.getRuntimeConfig(), request.sessionKey, request.agentId) : void 0;
if (requestedSession && !requestedSession.ok) {
respond(false, void 0, requestedSession.error);
return;
}
if (narrow && requestedSession?.ok) {
const agentError = authorizeGatewaySessionCreation({
cfg: context.getRuntimeConfig(),
client,
agentId: requestedSession.agentId
});
if (agentError) {
respond(false, void 0, agentError);
return;
}
}
const sessionKey = request.sessionKey && requestedSession?.ok ? resolveStoredSessionKeyForAgentStore({
cfg: context.getRuntimeConfig(),
agentId: requestedSession.agentId,
sessionKey: request.sessionKey
}) : void 0;
const create = (prepared) => {
if (sessionKey && requiresSharing()) {
const authorizationError = prepared?.authorizeMutation(client);
if (!prepared?.target || authorizationError) {
respond(false, void 0, authorizationError ?? errorShape(ErrorCodes.FORBIDDEN, "Session is unavailable."));
return;
}
}
if (narrow) {
authority.assertCurrent();
if (!sessionAccess || !prepared?.canAccess(client, "mutate", true, sessionAccess)) {
respond(false, void 0, errorShape(ErrorCodes.FORBIDDEN, "Session-scoped questions require your own materialized ordinary session."));
return;
}
}
const broadcastQuestion = (event, payload, observation, current, expectedRecord) => {
const scoped = sessionKey && context.getRuntimeConfig().gateway?.roles ? {
sessionKeys: [sessionKey],
...requestedSession?.ok ? { agentId: requestedSession.agentId } : {}
} : void 0;
let publishing = true;
const retained = questionBroadcastOptions({
observation,
prepared: current,
expectedRecord,
cfg: context.getRuntimeConfig(),
isPublishing: () => publishing
});
try {
if (scoped || retained) context.broadcast(event, payload, {
...scoped,
...retained
});
else context.broadcast(event, payload);
} finally {
publishing = false;
}
};
authority.assertCurrent();
const managerRequest = {
...request.id ? { id: request.id } : {},
questions: normalizeQuestions(request),
...requestedSession?.ok ? { agentId: requestedSession.agentId } : request.agentId ? { agentId: request.agentId } : {},
...sessionKey ? { sessionKey } : {},
...request.runId ? { runId: request.runId } : {},
timeoutMs: request.timeoutMs ?? DEFAULT_QUESTION_TIMEOUT_MS,
isRequesterActive,
sessionAccess,
requesterRun: requester?.operationalRunInstance,
registerHumanInputWait: requester && isRequesterActive ? (isPending) => registerActiveEmbeddedRunHumanInputWait(requester.delegatedAuthority, isPending) : void 0,
onResolved: async (event, observation) => {
handleQuestionChannelResolved(event);
let consumed = false;
try {
await withPreparedQuestionSessions(options, [{
...observation.record,
sessionAccess: observation.sessionAccess
}], ([current]) => {
consumed = true;
if (!observation.isCurrent()) return;
broadcastQuestion("question.resolved", event, observation, current);
}, { assertCurrent: () => {
if (!observation.isCurrent()) throw new Error("Question publication owner retired");
} });
} catch (error) {
if (consumed || !observation.isCurrent()) throw error;
broadcastQuestion("question.resolved", event, observation, void 0);
}
}
};
const record = manager.request(managerRequest);
accepted = true;
handleQuestionChannelRequested(record, scheduler);
broadcastQuestion("question.requested", record, manager.observe(record.id, record), prepared, record);
respond(true, {
id: record.id,
expiresAtMs: record.expiresAtMs
}, void 0);
};
if (sessionKey && requestedSession?.ok) {
let consumed = false;
try {
await withQuestionSessionAccess(options, sessionKey, requestedSession.agentId, (access, prepared) => {
consumed = true;
sessionAccess = request.questions.some((question) => question.isSecret || question.secretStore) ? void 0 : access;
try {
return create(prepared);
} finally {
if (!sessionAccess) access?.release();
}
}, {
assertCurrent: authority.assertCurrent,
includeMembers: !narrow && requiresSharing()
});
} catch (error) {
if (consumed || narrow || requiresSharing()) throw error;
create();
}
} else create();
} catch (error) {
if (error instanceof QuestionRequestValidationError) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message));
return;
}
if (!managerError(error, respond)) {
if (storeBound) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "Secret store entry metadata is unavailable."));
return;
}
throw error;
}
} finally {
if (!accepted) sessionAccess?.release();
}
},
"question.waitAnswer": async (options) => {
const { params, respond } = options;
if (!assertValidParams(params, validateQuestionWaitAnswerParams, "question.waitAnswer", respond)) return;
const request = params;
try {
const selected = prepareSelectedQuestion(options, request.id, "read");
if (!selected) return;
const { authorize } = selected;
const target = authorize.target;
const waiting = await withPreparedQuestionSessions(options, [target], ([prepared]) => {
const error = authorize.authorize(prepared);
if (error) {
respond(false, void 0, error);
return;
}
return { answer: manager.waitAnswer(request.id, request.timeoutMs, request.includeResolutionId) };
}, { assertCurrent: authorize.assertCurrent });
if (!waiting) return;
const answer = await waiting.answer;
await withPreparedQuestionSessions(options, [target], ([prepared]) => {
const error = authorize.authorize(prepared);
if (error) {
respond(false, void 0, error);
return;
}
respond(true, answer, void 0);
}, { assertCurrent: authorize.assertCurrent });
} catch (error) {
if (!managerError(error, respond)) throw error;
}
},
"question.resolve": async (options) => {
const { params, respond, client } = options;
if (!assertValidParams(params, validateQuestionResolveParams, "question.resolve", respond)) return;
const request = params;
try {
const selected = prepareSelectedQuestion(options, request.id, "mutate");
if (!selected) return;
const { question, authorize } = selected;
let reload;
await withPreparedQuestionSessions(options, [authorize.target], ([prepared]) => {
const authorizationError = authorize.authorize(prepared);
if (authorizationError) {
respond(false, void 0, authorizationError);
return;
}
if ("cancel" in request) {
respond(true, manager.cancel(request.id, request.resolvedBy), void 0);
return;
}
const secretQuestion = question.questions[0];
const binding = secretQuestion?.secretStore;
if (!binding) {
if (request.secretStoreAllowedHosts !== void 0) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Secret store allowed hosts require a store-bound question."));
return;
}
respond(true, manager.resolve(request.id, request.answers, request.resolvedBy, { resolutionId: request.resolutionId }), void 0);
return;
}
const submittedAnswers = request.answers.answers;
const values = Object.hasOwn(submittedAnswers, secretQuestion.questionId) ? submittedAnswers[secretQuestion.questionId] : void 0;
const value = values?.[0];
if (Object.keys(submittedAnswers).length !== 1 || values?.length !== 1 || value === void 0) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `question '${secretQuestion.questionId}' requires exactly one secret value`));
return;
}
registerSecretValueForRedaction(value);
const allowedHosts = request.secretStoreAllowedHosts ?? binding.allowedHosts;
let saved = false;
try {
const result = manager.resolve(request.id, { answers: { [secretQuestion.questionId]: ["stored"] } }, request.resolvedBy, {
resolutionId: request.resolutionId,
commit: () => {
storeWriteService.write({
name: binding.name,
value,
kind: "secret",
...allowedHosts !== void 0 ? { allowedHosts } : {},
updatedBy: storeWriteService.resolveUpdatedBy(client)
});
saved = true;
}
});
reload = {
name: binding.name,
result
};
} catch (error) {
if (managerError(error, respond)) return;
respond(false, void 0, errorShape(!saved && error instanceof SecretStoreValidationError ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, saved ? "Secret store entry was saved, but runtime refresh failed. Resolve provider errors and retry secrets.reload; do not resubmit this answer." : error instanceof SecretStoreValidationError ? error.message : "Secret store entry could not be saved."));
}
}, {
assertCurrent: authorize.assertCurrent,
includeMembers: !readGatewayRequestMutationAuthority(options).sessionScope && hasOperatorBoundary(client, options.context.getRuntimeConfig())
});
if (reload) try {
await storeWriteService.reloadReference(reload.name);
respond(true, reload.result, void 0);
} catch {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "Secret store entry was saved, but runtime refresh failed. Resolve provider errors and retry secrets.reload; do not resubmit this answer."));
}
} catch (error) {
if (!managerError(error, respond)) throw error;
}
},
"question.get": async (options) => {
const { params, respond } = options;
if (!assertValidParams(params, validateQuestionGetParams, "question.get", respond)) return;
const selected = prepareSelectedQuestion(options, params.id, "read");
if (!selected) return;
const { observation, authorize } = selected;
await withPreparedQuestionSessions(options, [authorize.target], ([prepared]) => {
const error = authorize.authorize(prepared);
if (error) {
respond(false, void 0, error);
return;
}
respond(true, { question: observation.record }, void 0);
}, { assertCurrent: authorize.assertCurrent }).catch((error) => {
if (!managerError(error, respond)) throw error;
});
},
"question.list": async (options) => {
const { params, respond } = options;
if (!assertValidParams(params, validateQuestionListParams, "question.list", respond)) return;
readGatewayRequestMutationAuthority(options).assertCurrent();
const records = manager.list(usesOwnRunQuestionAccess(options.client) ? (question) => canSelectQuestion(manager, question.id, options.client) : void 0).map((question) => {
const observation = manager.observe(question.id, question);
return {
question,
observation,
authorize: prepareQuestionAuthorization(options, observation, question.id, "read")
};
});
await withPreparedQuestionSessions(options, records.map(({ authorize }) => authorize.target), (prepared) => {
const questions = records.flatMap(({ question, observation, authorize }, index) => observation?.record === question && question.status === "pending" && !authorize.authorize(prepared[index]) ? [question] : []);
respond(true, { questions }, void 0);
}, { assertCurrent: readGatewayRequestMutationAuthority(options).assertCurrent });
}
};
}
//#endregion
export { createQuestionHandlers };