openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,387 lines (1,386 loc) • 48.3 kB
JavaScript
import { _ as uniqueStrings, l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { a as parseAccessGroupAllowFromEntry } from "./allow-from-Ddq5GeWG.js";
import { t as resolveCommandAuthorizedFromAuthorizers } from "./command-gating-65fgTdwb.js";
import { t as readChannelIngressStoreAllowFromForDmPolicy } from "./store-allow-from-CwbNJoLu.js";
import { t as resolveChannelIngressEffectiveAllowFromLists } from "./effective-allow-from-DBbOZ23Y.js";
import { n as resolveInboundMentionDecision } from "./mention-gating-3P8aSD7o.js";
//#region src/channels/message-access/allowlist.ts
/**
* Channel ingress allowlist diagnostics.
*
* Merges allowlists, applies mutable identifier policy, and redacts access-graph facts.
*/
/**
* Returns the first access-group related failure reason for an allowlist.
*/
function allowlistFailureReason(allowlist) {
if (allowlist.accessGroups.failed.length > 0) return "access_group_failed";
if (allowlist.accessGroups.unsupported.length > 0) return "access_group_unsupported";
if (allowlist.accessGroups.missing.length > 0) return "access_group_missing";
return null;
}
/**
* Projects an allowlist into redacted diagnostics safe for ingress access graphs.
*/
function redactedAllowlistDiagnostics(allowlist, reasonCode) {
return {
configured: allowlist.hasConfiguredEntries,
matched: allowlist.match.matched,
reasonCode,
matchedEntryIds: allowlist.matchedEntryIds,
invalidEntryCount: allowlist.invalidEntries.length,
disabledEntryCount: allowlist.disabledEntries.length,
accessGroups: allowlist.accessGroups
};
}
function mergeResolvedAllowlists(allowlists) {
const matches = allowlists.map((allowlist) => allowlist.match);
const matchedEntryIds = uniqueStrings(allowlists.flatMap((allowlist) => allowlist.matchedEntryIds));
return {
rawEntryCount: allowlists.reduce((sum, allowlist) => sum + allowlist.rawEntryCount, 0),
normalizedEntries: allowlists.flatMap((allowlist) => allowlist.normalizedEntries),
invalidEntries: allowlists.flatMap((allowlist) => allowlist.invalidEntries),
disabledEntries: allowlists.flatMap((allowlist) => allowlist.disabledEntries),
matchedEntryIds,
hasConfiguredEntries: allowlists.some((allowlist) => allowlist.hasConfiguredEntries),
hasMatchableEntries: allowlists.some((allowlist) => allowlist.hasMatchableEntries),
hasWildcard: allowlists.some((allowlist) => allowlist.hasWildcard),
accessGroups: {
referenced: uniqueStrings(allowlists.flatMap((allowlist) => allowlist.accessGroups.referenced)),
matched: uniqueStrings(allowlists.flatMap((allowlist) => allowlist.accessGroups.matched)),
missing: uniqueStrings(allowlists.flatMap((allowlist) => allowlist.accessGroups.missing)),
unsupported: uniqueStrings(allowlists.flatMap((allowlist) => allowlist.accessGroups.unsupported)),
failed: uniqueStrings(allowlists.flatMap((allowlist) => allowlist.accessGroups.failed))
},
match: {
matched: matches.some((match) => match.matched) || matchedEntryIds.length > 0,
matchedEntryIds
}
};
}
/**
* Applies mutable identifier matching policy to an already-resolved allowlist.
*/
function applyMutableIdentifierPolicy(allowlist, policy) {
if (policy.mutableIdentifierMatching === "enabled") return allowlist;
const dangerousEntryIds = new Set(allowlist.normalizedEntries.filter((entry) => entry.dangerous).map((entry) => entry.opaqueEntryId));
if (dangerousEntryIds.size === 0) return allowlist;
const matchedEntryIds = allowlist.matchedEntryIds.filter((id) => !dangerousEntryIds.has(id));
const disabledEntries = [...allowlist.disabledEntries, ...allowlist.normalizedEntries.filter((entry) => entry.dangerous).map((entry) => ({
opaqueEntryId: entry.opaqueEntryId,
reasonCode: "mutable_identifier_disabled"
}))];
return {
...allowlist,
disabledEntries,
matchedEntryIds,
hasMatchableEntries: allowlist.normalizedEntries.some((entry) => !entry.dangerous),
match: {
matched: matchedEntryIds.length > 0,
matchedEntryIds
}
};
}
/**
* Resolves the sender allowlist used for group/channel ingress after route overrides.
*/
function effectiveGroupSenderAllowlist(params) {
let effective = params.policy.groupAllowFromFallbackToAllowFrom && !params.state.allowlists.group.hasConfiguredEntries ? params.state.allowlists.dm : params.state.allowlists.group;
for (const route of params.state.routeFacts) {
if (route.gate !== "matched" || !route.senderAllowlist) continue;
if (route.senderPolicy === "inherit") {
effective = mergeResolvedAllowlists([effective, route.senderAllowlist]);
continue;
}
effective = route.senderAllowlist;
}
return applyMutableIdentifierPolicy(effective, params.policy);
}
//#endregion
//#region src/channels/message-access/sender-gates.ts
/**
* Channel ingress sender gate helpers.
*
* Evaluates DM and group sender policies against normalized allowlists.
*/
function senderGate(params) {
return {
id: params.id,
phase: "sender",
kind: params.kind,
effect: params.effect,
allowed: params.allowed,
reasonCode: params.reasonCode,
match: params.match,
sender: { policy: params.policy },
allowlist: redactedAllowlistDiagnostics(params.allowlistSource, params.reasonCode)
};
}
/**
* Evaluates direct-message sender policy against DM and pairing-store allowlists.
*/
function senderGateForDirect(params) {
const dm = applyMutableIdentifierPolicy(params.state.allowlists.dm, params.policy);
const pairingStore = applyMutableIdentifierPolicy(params.state.allowlists.pairingStore, params.policy);
const base = {
policy: params.policy.dmPolicy,
allowlistSource: dm,
match: dm.match
};
const allow = (reasonCode) => senderGate({
id: "sender:dm",
kind: "dmSender",
...base,
effect: "allow",
allowed: true,
reasonCode
});
const block = (reasonCode) => senderGate({
id: "sender:dm",
kind: "dmSender",
...base,
effect: "block-dispatch",
allowed: false,
reasonCode
});
if (params.policy.dmPolicy === "disabled") return block("dm_policy_disabled");
if (params.policy.dmPolicy === "open") {
if (dm.hasWildcard) return allow("dm_policy_open");
if (dm.match.matched) return allow("dm_policy_allowlisted");
return block("dm_policy_not_allowlisted");
}
if (dm.match.matched) return allow("dm_policy_allowlisted");
if (params.policy.dmPolicy === "pairing" && pairingStore.match.matched) return senderGate({
id: "sender:dm",
kind: "dmSender",
effect: "allow",
allowed: true,
reasonCode: "dm_policy_allowlisted",
match: pairingStore.match,
policy: params.policy.dmPolicy,
allowlistSource: pairingStore
});
if (params.policy.dmPolicy === "pairing" && params.state.event.mayPair) return block("dm_policy_pairing_required");
return block(params.policy.dmPolicy === "pairing" ? "event_pairing_not_allowed" : allowlistFailureReason(dm) ?? "dm_policy_not_allowlisted");
}
/**
* Evaluates group/channel sender policy after route sender allowlist overrides are applied.
*/
function senderGateForGroup(params) {
const group = effectiveGroupSenderAllowlist(params);
const base = {
policy: params.policy.groupPolicy,
allowlistSource: group,
match: group.match
};
const allow = (reasonCode) => senderGate({
id: "sender:group",
kind: "groupSender",
...base,
effect: "allow",
allowed: true,
reasonCode
});
const block = (reasonCode) => senderGate({
id: "sender:group",
kind: "groupSender",
...base,
effect: "block-dispatch",
allowed: false,
reasonCode
});
if (params.policy.groupPolicy === "disabled") return block("group_policy_disabled");
if (params.policy.groupPolicy === "open") return allow("group_policy_open");
if (!group.hasConfiguredEntries) return block("group_policy_empty_allowlist");
if (group.match.matched) return allow("group_policy_allowed");
return block(allowlistFailureReason(group) ?? "group_policy_not_allowlisted");
}
/**
* Applies event auth mode to sender gates for non-message callbacks.
*/
function applyEventAuthModeToSenderGate(params) {
if (params.state.event.authMode === "inbound" || params.senderGate.allowed) return params.senderGate;
const reasonCode = "sender_not_required";
return {
...params.senderGate,
effect: "ignore",
allowed: true,
reasonCode,
allowlist: params.senderGate.allowlist ? {
...params.senderGate.allowlist,
reasonCode
} : void 0
};
}
//#endregion
//#region src/channels/message-access/decision.ts
/**
* Channel ingress decision graph builder.
*
* Evaluates route, sender, command, and mention gates into one admission decision.
*/
function decisiveDecision(params) {
return {
admission: params.admission,
decision: params.decision,
decisiveGateId: params.gate.id,
reasonCode: params.gate.reasonCode,
graph: { gates: params.gates }
};
}
function routeGates(state) {
return state.routeFacts.map((route) => ({
id: route.id,
phase: "route",
kind: route.kind,
effect: route.effect,
allowed: route.effect !== "block-dispatch",
reasonCode: route.effect === "block-dispatch" ? "route_blocked" : "allowed",
match: route.match
}));
}
function routeSenderEmptyGate(state) {
const route = state.routeFacts.find((fact) => fact.senderPolicy === "deny-when-empty" && fact.gate === "matched" && fact.senderAllowlist?.hasConfiguredEntries !== true);
if (!route) return null;
const reasonCode = "route_sender_empty";
return {
id: `${route.id}:sender`,
phase: "route",
kind: "routeSender",
effect: "block-dispatch",
allowed: false,
reasonCode,
match: route.match,
allowlist: route.senderAllowlist ? redactedAllowlistDiagnostics(route.senderAllowlist, reasonCode) : void 0
};
}
function commandGate(params) {
const command = params.policy.command;
if (!command) return {
id: "command",
phase: "command",
kind: "command",
effect: "allow",
allowed: true,
reasonCode: "command_authorized"
};
const useAccessGroups = command.useAccessGroups ?? true;
const owner = applyMutableIdentifierPolicy(params.state.allowlists.commandOwner, params.policy);
const group = applyMutableIdentifierPolicy(params.state.allowlists.commandGroup, params.policy);
const authorized = resolveCommandAuthorizedFromAuthorizers({
useAccessGroups,
modeWhenAccessGroupsOff: command.modeWhenAccessGroupsOff,
authorizers: [{
configured: owner.hasConfiguredEntries,
allowed: owner.match.matched
}, {
configured: group.hasConfiguredEntries,
allowed: group.match.matched
}]
});
const shouldBlock = command.allowTextCommands && command.hasControlCommand && !authorized;
return {
id: "command",
phase: "command",
kind: "command",
effect: shouldBlock ? "block-command" : "allow",
allowed: authorized,
reasonCode: shouldBlock ? "control_command_unauthorized" : "command_authorized",
match: mergeCommandMatch(owner.match, group.match),
command: {
useAccessGroups,
allowTextCommands: command.allowTextCommands,
modeWhenAccessGroupsOff: command.modeWhenAccessGroupsOff,
shouldBlockControlCommand: shouldBlock
}
};
}
function mergeCommandMatch(owner, group) {
const matchedEntryIds = uniqueStrings([...owner.matchedEntryIds, ...group.matchedEntryIds]);
return {
matched: owner.matched || group.matched || matchedEntryIds.length > 0,
matchedEntryIds
};
}
function eventGate(params) {
const authMode = params.state.event.authMode;
const event = params.state.event;
const eventResult = (allowed, reasonCode) => ({
id: "event",
phase: "event",
kind: "event",
effect: allowed ? "allow" : "block-dispatch",
allowed,
reasonCode,
event
});
if (authMode === "none" || authMode === "route-only") return eventResult(true, "event_authorized");
if (authMode === "command") return eventResult(params.commandGate.allowed, params.commandGate.allowed ? "event_authorized" : "event_unauthorized");
if (authMode === "origin-subject") {
if (!params.state.event.hasOriginSubject) return eventResult(false, "origin_subject_missing");
const matched = params.state.event.originSubjectMatched;
return eventResult(matched, matched ? "event_authorized" : "origin_subject_not_matched");
}
return eventResult(params.senderGate.allowed, params.senderGate.allowed ? "event_authorized" : "event_unauthorized");
}
function activationMetadata(params) {
const mentionFacts = params.mentionFacts;
return {
hasMentionFacts: mentionFacts != null,
requireMention: params.activation?.requireMention ?? false,
allowTextCommands: params.activation?.allowTextCommands ?? false,
...params.activation?.allowedImplicitMentionKinds !== void 0 ? { allowedImplicitMentionKinds: params.activation.allowedImplicitMentionKinds } : {},
...params.activation?.order ? { order: params.activation.order } : {},
shouldSkip: params.shouldSkip,
...mentionFacts?.canDetectMention !== void 0 ? { canDetectMention: mentionFacts.canDetectMention } : {},
...mentionFacts?.wasMentioned !== void 0 ? { wasMentioned: mentionFacts.wasMentioned } : {},
...mentionFacts?.hasAnyMention !== void 0 ? { hasAnyMention: mentionFacts.hasAnyMention } : {},
...mentionFacts?.implicitMentionKinds !== void 0 ? { implicitMentionKinds: mentionFacts.implicitMentionKinds } : {},
...params.effectiveWasMentioned !== void 0 ? { effectiveWasMentioned: params.effectiveWasMentioned } : {},
...params.shouldBypassMention !== void 0 ? { shouldBypassMention: params.shouldBypassMention } : {}
};
}
function activationGate(params) {
const activation = params.policy.activation;
const mentionFacts = params.state.mentionFacts;
const activationResult = (input) => ({
id: "activation",
phase: "activation",
kind: "mention",
effect: input.shouldSkip ? "skip" : "allow",
allowed: !input.shouldSkip,
reasonCode: input.shouldSkip ? "activation_skipped" : "activation_allowed",
activation: activationMetadata({
activation,
mentionFacts,
shouldSkip: input.shouldSkip,
effectiveWasMentioned: input.effectiveWasMentioned,
shouldBypassMention: input.shouldBypassMention
})
});
if (!activation || !mentionFacts) return activationResult({
shouldSkip: false,
effectiveWasMentioned: mentionFacts && (mentionFacts.wasMentioned || Boolean(mentionFacts.implicitMentionKinds?.length))
});
const result = resolveInboundMentionDecision({
facts: mentionFacts,
policy: {
isGroup: params.state.conversationKind !== "direct",
requireMention: activation.requireMention,
allowedImplicitMentionKinds: activation.allowedImplicitMentionKinds,
allowTextCommands: activation.allowTextCommands,
hasControlCommand: params.policy.command?.hasControlCommand ?? false,
commandAuthorized: params.commandGate.allowed
}
});
return activationResult({
shouldSkip: result.shouldSkip,
effectiveWasMentioned: result.effectiveWasMentioned,
shouldBypassMention: result.shouldBypassMention
});
}
function decideChannelIngress(state, policy) {
const gates = routeGates(state);
const emptyRouteSenderGate = routeSenderEmptyGate(state);
if (emptyRouteSenderGate) gates.push(emptyRouteSenderGate);
const routeBlock = gates.find((entry) => entry.effect === "block-dispatch");
if (routeBlock) return decisiveDecision({
admission: "drop",
decision: "block",
gate: routeBlock,
gates
});
const activationBeforeSender = policy.activation?.order === "before-sender" && !policy.activation.allowTextCommands ? activationGate({
state,
policy,
commandGate: commandGate({
state,
policy: {
...policy,
command: void 0
}
})
}) : null;
if (activationBeforeSender) {
gates.push(activationBeforeSender);
if (activationBeforeSender.effect === "skip") return decisiveDecision({
admission: "skip",
decision: "allow",
gate: activationBeforeSender,
gates
});
}
const eventModeSender = applyEventAuthModeToSenderGate({
state,
senderGate: state.conversationKind === "direct" ? senderGateForDirect({
state,
policy
}) : senderGateForGroup({
state,
policy
})
});
gates.push(eventModeSender);
if (!eventModeSender.allowed) return decisiveDecision({
admission: eventModeSender.reasonCode === "dm_policy_pairing_required" ? "pairing-required" : "drop",
decision: eventModeSender.reasonCode === "dm_policy_pairing_required" ? "pairing" : "block",
gate: eventModeSender,
gates
});
const command = commandGate({
state,
policy
});
gates.push(command);
if (command.effect === "block-command") return decisiveDecision({
admission: "drop",
decision: "block",
gate: command,
gates
});
const event = eventGate({
state,
senderGate: eventModeSender,
commandGate: command
});
gates.push(event);
if (!event.allowed) return decisiveDecision({
admission: "drop",
decision: "block",
gate: event,
gates
});
const activation = activationBeforeSender ?? activationGate({
state,
policy,
commandGate: command
});
if (!activationBeforeSender) gates.push(activation);
if (activation.effect === "skip") return decisiveDecision({
admission: "skip",
decision: "allow",
gate: activation,
gates
});
if (activation.effect === "observe") return decisiveDecision({
admission: "observe",
decision: "allow",
gate: activation,
gates
});
return decisiveDecision({
admission: "dispatch",
decision: "allow",
gate: activation,
gates
});
}
//#endregion
//#region src/channels/message-access/runtime-identity.ts
/** Build an identity descriptor for channels with one stable id and optional aliases. */
function defineStableChannelIngressIdentity(params = {}) {
const { entryIdPrefix, resolveEntryId, aliases, isWildcardEntry, matchEntry, ...primary } = params;
return {
primary,
aliases,
isWildcardEntry,
matchEntry,
resolveEntryId: resolveEntryId ?? (entryIdPrefix ? ({ entryIndex }) => `${entryIdPrefix}-${entryIndex + 1}` : void 0)
};
}
function defaultNormalize(value) {
return value;
}
function normalizeFieldValue(field, value, mode) {
const normalized = (mode === "entry" ? field.normalizeEntry ?? field.normalize ?? defaultNormalize : field.normalizeSubject ?? field.normalize ?? defaultNormalize)(value);
return normalized == null ? null : normalized.trim() || null;
}
function fieldDangerous(field, value) {
return typeof field.dangerous === "function" ? field.dangerous(value) : field.dangerous;
}
function identityFields(identity) {
const fields = [{
...identity.primary,
key: identity.primary.key ?? "stableId",
kind: identity.primary.kind ?? "stable-id"
}];
for (const alias of identity.aliases ?? []) fields.push({
...alias,
kind: alias.kind ?? `plugin:${alias.key}`
});
return fields;
}
function identityMatchKey(entry) {
return `${entry.kind}:${entry.value}`;
}
function adapterEntry(params) {
return {
opaqueEntryId: params.identity.resolveEntryId?.({
entry: params.entry,
entryIndex: params.entryIndex,
fieldKey: params.field.key,
fieldIndex: params.fieldIndex
}) ?? `entry-${params.entryIndex + 1}:${params.fallbackSuffix ?? params.field.key}`,
kind: params.field.kind,
value: params.value,
dangerous: fieldDangerous(params.field, params.entry),
sensitivity: params.field.sensitivity
};
}
function createIdentityAdapter(identity) {
const fields = identityFields(identity);
const isWildcardEntry = identity.isWildcardEntry ?? ((value) => value === "*");
return {
normalizeEntries({ entries }) {
return {
matchable: entries.flatMap((entry, entryIndex) => {
if (isWildcardEntry(entry)) return [adapterEntry({
identity,
field: fields[0],
fieldIndex: 0,
entry,
entryIndex,
value: "*",
fallbackSuffix: "wildcard"
})];
return fields.flatMap((field, fieldIndex) => {
const value = normalizeFieldValue(field, entry, "entry");
if (!value) return [];
return [adapterEntry({
identity,
field,
fieldIndex,
entry,
entryIndex,
value
})];
});
}),
invalid: [],
disabled: []
};
},
matchSubject({ subject, entries, context }) {
const subjectKeys = new Set(subject.identifiers.flatMap((identifier) => {
const field = fields.find((candidate) => candidate.kind === identifier.kind);
if (!field) return [];
const value = normalizeFieldValue(field, identifier.value, "subject");
return value ? [identityMatchKey({
kind: identifier.kind,
value
})] : [];
}));
const matchedEntryIds = entries.filter((entry) => {
const fallback = entry.value === "*" || subjectKeys.has(identityMatchKey(entry));
return identity.matchEntry?.({
subject,
entry,
context
}) ?? fallback;
}).map((entry) => entry.opaqueEntryId);
return {
matched: matchedEntryIds.length > 0,
matchedEntryIds
};
}
};
}
function createIdentitySubject(identity, input) {
return { identifiers: identityFields(identity).flatMap((field, index) => {
const rawValue = index === 0 ? input.stableId : input.aliases?.[field.key];
if (rawValue == null) return [];
const value = String(rawValue);
return [{
opaqueId: field.key,
kind: field.kind,
value,
dangerous: fieldDangerous(field, value),
sensitivity: field.sensitivity
}];
}) };
}
//#endregion
//#region src/channels/message-access/runtime-access-groups.ts
/**
* Runtime access-group resolution for channel ingress.
*
* Preserves symbolic access-group entries until dynamic membership facts are available.
*/
function accessGroupNames(entries) {
return uniqueStrings(entries.map((entry) => parseAccessGroupAllowFromEntry(String(entry))).filter((entry) => entry != null));
}
/**
* Lists every access-group name referenced by grouped allowFrom entry arrays.
*/
function allReferencedAccessGroupNames(entries) {
return uniqueStrings(entries.flatMap((entryGroup) => accessGroupNames(entryGroup)));
}
/**
* Normalizes direct sender entries while preserving access-group references for runtime lookup.
*/
async function normalizeEffectiveEntries(params) {
const rawEntries = normalizeStringEntries(params.entries);
const accessGroupEntries = rawEntries.filter((entry) => parseAccessGroupAllowFromEntry(entry) != null);
const directEntries = rawEntries.filter((entry) => parseAccessGroupAllowFromEntry(entry) == null);
if (directEntries.length === 0) return accessGroupEntries;
const normalized = await params.adapter.normalizeEntries({
entries: directEntries,
context: params.context,
accountId: params.accountId
});
return uniqueStrings([...accessGroupEntries, ...normalized.matchable.map((entry) => entry.value)]);
}
/**
* Resolves dynamic access-group membership facts for referenced runtime access groups.
*/
async function resolveRuntimeAccessGroupMembershipFacts(params) {
if (!params.input.resolveAccessGroupMembership || params.names.length === 0) return [];
const facts = [];
for (const name of params.names) {
const group = params.input.accessGroups?.[name];
if (!group || group.type === "message.senders") continue;
try {
const matched = await params.input.resolveAccessGroupMembership({
name,
group,
channelId: params.channelId,
accountId: params.input.accountId,
subject: params.input.subject
});
facts.push(matched ? {
kind: "matched",
groupName: name,
source: "dynamic",
matchedEntryIds: [`access-group:${name}`]
} : {
kind: "not-matched",
groupName: name,
source: "dynamic"
});
} catch {
facts.push({
kind: "failed",
groupName: name,
source: "dynamic",
reasonCode: "access_group_failed",
diagnosticId: `access-group:${name}`
});
}
}
return facts;
}
//#endregion
//#region src/channels/message-access/state.ts
/**
* Channel ingress state resolver.
*
* Normalizes and matches route, sender, command, and access-group allowlists.
*/
function redactedEntries(entries) {
return entries.map(({ value: _value, ...entry }) => entry);
}
function emptyMatch() {
return {
matched: false,
matchedEntryIds: []
};
}
function mergeMatches(matches) {
const matchedEntryIds = uniqueStrings(matches.flatMap((match) => match.matchedEntryIds));
return {
matched: matches.some((match) => match.matched) || matchedEntryIds.length > 0,
matchedEntryIds
};
}
function mergeDiagnostics(...groups) {
const diagnostics = [];
for (const group of groups) if (group) diagnostics.push(...group);
return diagnostics;
}
function accessGroupFactByName(facts) {
return new Map((facts ?? []).map((fact) => [fact.groupName, fact]));
}
async function normalizeAndMatch(params) {
if (params.entries.length === 0) return {
normalizedEntries: [],
invalidEntries: [],
disabledEntries: [],
match: emptyMatch()
};
const normalized = await params.adapter.normalizeEntries({
entries: params.entries,
context: params.context,
accountId: params.accountId
});
const match = normalized.matchable.length > 0 ? await params.adapter.matchSubject({
subject: params.subject,
entries: normalized.matchable,
context: params.context
}) : emptyMatch();
return {
normalizedEntries: redactedEntries(normalized.matchable),
invalidEntries: normalized.invalid,
disabledEntries: normalized.disabled,
match
};
}
function referencedAccessGroups(entries) {
return Array.from(new Set(entries.map((entry) => parseAccessGroupAllowFromEntry(entry)).filter((entry) => entry != null)));
}
function directAllowlistEntries(entries) {
return entries.filter((entry) => parseAccessGroupAllowFromEntry(entry) == null);
}
function groupSenderEntries(params) {
const group = params.input.accessGroups?.[params.groupName];
if (!group || group.type !== "message.senders") return [];
return normalizeStringEntries([...group.members["*"] ?? [], ...group.members[params.input.channelId] ?? []]);
}
function eventSubjectMatchContext(input) {
return input.conversation.kind === "direct" ? "dm" : "group";
}
async function normalizeSubjectIdentifiersForMatch(params) {
return (await Promise.all(params.subject.identifiers.map(async (identifier, identifierIndex) => {
return (await params.input.adapter.normalizeEntries({
entries: [identifier.value],
context: params.context,
accountId: params.input.accountId
})).matchable.filter((entry) => entry.kind === identifier.kind && entry.value !== "*").map((entry, entryIndex) => ({
opaqueEntryId: `${params.opaquePrefix}-${identifierIndex + 1}:${entryIndex + 1}`,
kind: entry.kind,
value: entry.value,
dangerous: entry.dangerous,
sensitivity: entry.sensitivity
}));
}))).flat();
}
async function originSubjectMatched(input) {
const origin = input.event.originSubject;
if (!origin) return false;
if (origin.identifiers.some((identifier) => input.subject.identifiers.some((current) => current.kind === identifier.kind && current.value === identifier.value))) return true;
const context = eventSubjectMatchContext(input);
const originEntries = await normalizeSubjectIdentifiersForMatch({
input,
subject: origin,
context,
opaquePrefix: "origin"
});
if (originEntries.length > 0) {
if ((await input.adapter.matchSubject({
subject: input.subject,
entries: originEntries,
context
})).matched) return true;
}
const currentEntries = await normalizeSubjectIdentifiersForMatch({
input,
subject: input.subject,
context,
opaquePrefix: "current"
});
if (currentEntries.length === 0) return false;
return (await input.adapter.matchSubject({
subject: origin,
entries: currentEntries,
context
})).matched;
}
async function resolveAccessGroupEntries(params) {
const factByName = accessGroupFactByName(params.input.accessGroupMembership);
const accessGroups = {
referenced: [...params.referenced],
matched: [],
missing: [],
unsupported: [],
failed: []
};
const normalizedEntries = [];
const invalidEntries = [];
const disabledEntries = [];
const matches = [];
for (const groupName of params.referenced) {
const fact = factByName.get(groupName);
if (fact?.kind === "matched") {
accessGroups.matched.push(groupName);
matches.push({
matched: true,
matchedEntryIds: fact.matchedEntryIds
});
continue;
}
if (fact?.kind === "missing" || fact?.kind === "unsupported" || fact?.kind === "failed") {
accessGroups[fact.kind].push(groupName);
continue;
}
if (fact?.kind === "not-matched") continue;
const group = params.input.accessGroups?.[groupName];
if (!group) {
accessGroups.missing.push(groupName);
continue;
}
if (group.type !== "message.senders") {
accessGroups.unsupported.push(groupName);
continue;
}
const groupEntries = groupSenderEntries({
groupName,
input: params.input
});
const resolved = await normalizeAndMatch({
adapter: params.input.adapter,
subject: params.input.subject,
accountId: params.input.accountId,
entries: groupEntries,
context: params.context
});
normalizedEntries.push(...resolved.normalizedEntries);
invalidEntries.push(...resolved.invalidEntries);
disabledEntries.push(...resolved.disabledEntries);
if (resolved.match.matched) {
accessGroups.matched.push(groupName);
matches.push(resolved.match);
}
}
return {
normalizedEntries,
invalidEntries,
disabledEntries,
matches,
accessGroups
};
}
async function resolveIngressAllowlist(params) {
const entries = normalizeStringEntries(params.rawEntries ?? []);
const referenced = referencedAccessGroups(entries);
const directEntries = directAllowlistEntries(entries);
const direct = await normalizeAndMatch({
adapter: params.input.adapter,
subject: params.input.subject,
accountId: params.input.accountId,
entries: directEntries,
context: params.context
});
const groups = await resolveAccessGroupEntries({
input: params.input,
context: params.context,
referenced
});
const match = mergeMatches([direct.match, ...groups.matches]);
return {
rawEntryCount: entries.length,
normalizedEntries: [...direct.normalizedEntries, ...groups.normalizedEntries],
invalidEntries: mergeDiagnostics(direct.invalidEntries, groups.invalidEntries),
disabledEntries: mergeDiagnostics(direct.disabledEntries, groups.disabledEntries),
matchedEntryIds: match.matchedEntryIds,
hasConfiguredEntries: entries.length > 0,
hasMatchableEntries: direct.normalizedEntries.length > 0 || groups.normalizedEntries.length > 0,
hasWildcard: directEntries.includes("*"),
accessGroups: groups.accessGroups,
match
};
}
async function resolveRouteFacts(input) {
const routeFacts = [...input.routeFacts ?? []].toSorted((left, right) => left.precedence - right.precedence || left.id.localeCompare(right.id));
const resolved = [];
for (const route of routeFacts) {
const senderAllowFrom = route.senderAllowFrom ?? (route.senderAllowFromSource === "effective-dm" ? input.allowlists.dm : route.senderAllowFromSource === "effective-group" ? input.allowlists.group : void 0);
resolved.push({
id: route.id,
kind: route.kind,
gate: route.gate,
effect: route.effect,
precedence: route.precedence,
senderPolicy: route.senderPolicy,
match: route.match,
senderAllowlist: senderAllowFrom != null ? await resolveIngressAllowlist({
input,
rawEntries: senderAllowFrom,
context: "route"
}) : void 0
});
}
return resolved;
}
async function resolveChannelIngressState(input) {
const [dm, pairingStore, group, commandOwner, commandGroup, routeFacts, eventOriginMatched] = await Promise.all([
resolveIngressAllowlist({
input,
rawEntries: input.allowlists.dm,
context: "dm"
}),
resolveIngressAllowlist({
input,
rawEntries: input.allowlists.pairingStore,
context: "dm"
}),
resolveIngressAllowlist({
input,
rawEntries: input.allowlists.group,
context: "group"
}),
resolveIngressAllowlist({
input,
rawEntries: input.allowlists.commandOwner,
context: "command"
}),
resolveIngressAllowlist({
input,
rawEntries: input.allowlists.commandGroup,
context: "command"
}),
resolveRouteFacts(input),
originSubjectMatched(input)
]);
return {
channelId: input.channelId,
accountId: input.accountId,
conversationKind: input.conversation.kind,
event: {
kind: input.event.kind,
authMode: input.event.authMode,
mayPair: input.event.mayPair,
hasOriginSubject: input.event.originSubject != null,
originSubjectMatched: eventOriginMatched
},
mentionFacts: input.mentionFacts,
routeFacts,
allowlists: {
dm,
pairingStore,
group,
commandOwner,
commandGroup
}
};
}
//#endregion
//#region src/channels/message-access/runtime.ts
/**
* Channel ingress runtime resolver.
*
* Merges route, sender, command, access-group, and pairing-store facts before decision evaluation.
*/
function shouldReadStore(params) {
return params.conversationKind === "direct" && params.dmPolicy !== "allowlist" && params.dmPolicy !== "open";
}
async function readStoreAllowFrom(params) {
if (!shouldReadStore({
conversationKind: params.conversation.kind,
dmPolicy: params.policy.dmPolicy
})) return [];
return [...(params.readStoreAllowFrom ? await params.readStoreAllowFrom({
channelId: params.channelId,
accountId: params.accountId,
dmPolicy: params.policy.dmPolicy
}).catch(() => []) : params.useDefaultPairingStore ? await readChannelIngressStoreAllowFromForDmPolicy({
provider: params.channelId,
accountId: params.accountId,
dmPolicy: params.policy.dmPolicy
}) : []) ?? []];
}
function commandRequested(policy) {
return policy.command != null;
}
function normalizeChannelId(id) {
const trimmed = id.trim();
if (!trimmed) throw new Error("Channel ingress channel id must be non-empty.");
return trimmed;
}
function findIngressGate(params) {
return params.ingress.graph.gates.find((gate) => gate.phase === params.phase && gate.kind === params.kind);
}
function findSenderGate(ingress, isGroup) {
return findIngressGate({
ingress,
phase: "sender",
kind: isGroup ? "groupSender" : "dmSender"
});
}
function useAccessGroupsFromConfig(params) {
return params.useAccessGroups ?? params.cfg?.commands?.useAccessGroups !== false;
}
function channelIngressCommand(params = {}) {
if (params.requested === false) return;
const { requested: _requested, cfg, ...command } = params;
return {
...command,
useAccessGroups: useAccessGroupsFromConfig({
useAccessGroups: params.useAccessGroups,
cfg
}),
allowTextCommands: params.allowTextCommands ?? false,
hasControlCommand: params.hasControlCommand ?? true
};
}
function channelIngressEvent(params = {}) {
const isGroup = params.isGroup ?? false;
return {
kind: params.kind ?? "message",
authMode: params.authMode ?? "inbound",
mayPair: params.mayPair ?? !isGroup,
...params.originSubject ? { originSubject: params.originSubject } : {}
};
}
function resolveCommandInput(params) {
if (params.command === false || params.command == null) return;
return channelIngressCommand({
...params.command,
useAccessGroups: params.command.useAccessGroups ?? params.useAccessGroups
});
}
function resolveResolverPolicy(params) {
return {
dmPolicy: params.input.dmPolicy ?? params.base.defaultDmPolicy ?? "pairing",
groupPolicy: params.input.groupPolicy ?? params.base.defaultGroupPolicy ?? "disabled",
groupAllowFromFallbackToAllowFrom: params.input.policy?.groupAllowFromFallbackToAllowFrom ?? params.base.groupAllowFromFallbackToAllowFrom,
mutableIdentifierMatching: params.input.policy?.mutableIdentifierMatching ?? params.base.mutableIdentifierMatching,
...params.input.policy?.activation ? { activation: params.input.policy.activation } : {}
};
}
/**
* Create a reusable ingress resolver for one channel account and identity
* descriptor.
*/
function createChannelIngressResolver(base) {
const resolve = async (input, eventDefaults) => {
const isGroup = input.conversation.kind !== "direct";
const useAccessGroups = useAccessGroupsFromConfig({
useAccessGroups: base.useAccessGroups,
cfg: base.cfg
});
return await resolveChannelMessageIngress({
channelId: base.channelId,
accountId: base.accountId,
identity: base.identity,
subject: input.subject,
conversation: input.conversation,
event: channelIngressEvent({
isGroup,
...eventDefaults,
...input.event
}),
policy: resolveResolverPolicy({
base,
input
}),
allowFrom: input.allowFrom,
groupAllowFrom: input.groupAllowFrom,
route: input.route,
routeFacts: input.routeFacts,
accessGroups: base.accessGroups ?? base.cfg?.accessGroups,
accessGroupMembership: [...base.accessGroupMembership ?? [], ...input.accessGroupMembership ?? []],
resolveAccessGroupMembership: base.resolveAccessGroupMembership,
accessGroupMatchedAllowFromEntry: base.accessGroupMatchedAllowFromEntry,
providerMissingFallbackApplied: input.providerMissingFallbackApplied,
mentionFacts: input.mentionFacts,
readStoreAllowFrom: base.readStoreAllowFrom,
useDefaultPairingStore: base.useDefaultPairingStore,
command: resolveCommandInput({
command: input.command,
useAccessGroups
})
});
};
return {
message: async (input) => await resolve(input),
command: async (input) => await resolve(input, {
authMode: "command",
mayPair: false
}),
event: async (input) => await resolve(input, { mayPair: false })
};
}
/**
* Resolve one inbound event using a simple stable subject identity descriptor.
*/
async function resolveStableChannelMessageIngress(params) {
return await createChannelIngressResolver({
...params,
identity: defineStableChannelIngressIdentity(params.identity)
}).message(params);
}
function routeDescriptors(route) {
if (!route) return [];
if (Array.isArray(route)) return [...route];
return [route];
}
/**
* Collect optional route descriptors while dropping false, null, and undefined
* entries.
*/
function channelIngressRoutes(...routes) {
return routes.filter((route) => Boolean(route));
}
function routeDescriptorMatch(descriptor) {
const matched = descriptor.matched ?? descriptor.allowed ?? descriptor.enabled !== false;
return {
matched,
matchedEntryIds: matched && descriptor.matchId ? [descriptor.matchId] : []
};
}
function routeFact(params) {
return {
id: params.id,
kind: params.kind ?? "route",
gate: params.gate,
effect: params.effect,
precedence: params.precedence ?? 0,
senderPolicy: params.senderPolicy ?? "inherit",
senderAllowFrom: params.senderAllowFrom,
senderAllowFromSource: params.senderAllowFromSource,
match: params.match
};
}
function routeFactDefaults(descriptor) {
return {
id: descriptor.id,
...descriptor.kind ? { kind: descriptor.kind } : {},
...descriptor.precedence !== void 0 ? { precedence: descriptor.precedence } : {},
...descriptor.senderPolicy ? { senderPolicy: descriptor.senderPolicy } : {},
...descriptor.senderAllowFrom != null ? { senderAllowFrom: [...descriptor.senderAllowFrom] } : {},
...descriptor.senderAllowFromSource ? { senderAllowFromSource: descriptor.senderAllowFromSource } : {},
match: routeDescriptorMatch(descriptor)
};
}
function routeFactsFromDescriptors(route) {
return routeDescriptors(route).flatMap((descriptor) => {
if (descriptor.configured === false) return [];
const defaults = routeFactDefaults(descriptor);
if (descriptor.enabled === false) return [routeFact({
...defaults,
gate: "disabled",
effect: "block-dispatch"
})];
if (descriptor.allowed !== void 0) return [routeFact({
...defaults,
gate: descriptor.allowed ? "matched" : "not-matched",
effect: descriptor.allowed ? "allow" : "block-dispatch"
})];
if (descriptor.senderPolicy !== "deny-when-empty" && descriptor.senderAllowFrom == null && descriptor.senderAllowFromSource == null) return [];
return [routeFact({
...defaults,
kind: descriptor.senderPolicy === "deny-when-empty" ? defaults.kind : "routeSender",
gate: "matched",
effect: "allow",
senderPolicy: descriptor.senderPolicy === "deny-when-empty" ? "deny-when-empty" : defaults.senderPolicy
})];
});
}
function routeDescriptorForGate(params) {
const baseGateId = params.gate.id.endsWith(":sender") ? params.gate.id.slice(0, -7) : params.gate.id;
return params.descriptors.find((descriptor) => descriptor.id === params.gate.id || descriptor.id === baseGateId);
}
function projectRouteAccess(params) {
const descriptors = routeDescriptors(params.route);
const routeBlock = params.ingress.graph.gates.find((entry) => entry.phase === "route" && entry.effect === "block-dispatch");
if (routeBlock) {
const descriptor = routeDescriptorForGate({
descriptors,
gate: routeBlock
});
return {
allowed: routeBlock.allowed,
reasonCode: routeBlock.reasonCode,
...descriptor?.blockReason ? { reason: descriptor.blockReason } : {},
gate: routeBlock
};
}
const routeSenderReplacement = descriptors.find((descriptor) => descriptor.senderPolicy === "replace" && descriptor.blockReason);
const senderBlock = params.ingress.graph.gates.find((entry) => entry.phase === "sender" && entry.effect === "block-dispatch");
if (routeSenderReplacement && senderBlock) return {
allowed: false,
reasonCode: senderBlock.reasonCode,
reason: routeSenderReplacement.blockReason,
gate: senderBlock
};
const gate = params.ingress.graph.gates.find((entry) => entry.phase === "route");
if (gate) return {
allowed: gate.allowed,
reasonCode: gate.reasonCode,
gate
};
return { allowed: true };
}
function projectSenderAccess(params) {
const gate = findSenderGate(params.ingress, params.isGroup);
const reasonCode = !gate && params.isGroup && params.ingress.reasonCode === "route_sender_empty" && params.effectiveGroupAllowFrom.length === 0 ? "group_policy_empty_allowlist" : gate?.reasonCode ?? params.ingress.reasonCode;
const decision = reasonCode === "dm_policy_pairing_required" ? "pairing" : gate?.allowed === true ? "allow" : "block";
return {
allowed: decision === "allow",
decision,
reasonCode,
...gate ? { gate } : {},
effectiveAllowFrom: params.effectiveAllowFrom,
effectiveGroupAllowFrom: params.effectiveGroupAllowFrom,
providerMissingFallbackApplied: params.providerMissingFallbackApplied ?? false
};
}
function projectCommandAccess(params) {
const gate = findIngressGate({
ingress: params.ingress,
phase: "command",
kind: "command"
});
return {
requested: commandRequested(params.policy),
authorized: commandRequested(params.policy) ? gate?.allowed === true : false,
shouldBlockControlCommand: gate?.command?.shouldBlockControlCommand === true,
reasonCode: gate?.reasonCode ?? params.ingress.reasonCode,
...gate ? { gate } : {}
};
}
function projectActivationAccess(params) {
const gate = findIngressGate({
ingress: params.ingress,
phase: "activation",
kind: "mention"
});
return {
ran: gate != null,
allowed: gate?.allowed === true,
shouldSkip: gate?.activation?.shouldSkip === true,
reasonCode: gate?.reasonCode ?? params.ingress.reasonCode,
...gate?.activation?.effectiveWasMentioned !== void 0 ? { effectiveWasMentioned: gate.activation.effectiveWasMentioned } : {},
...gate?.activation?.shouldBypassMention !== void 0 ? { shouldBypassMention: gate.activation.shouldBypassMention } : {},
...gate ? { gate } : {}
};
}
function commandOwnerAllowFrom(params) {
if (params.command?.commandOwnerAllowFrom != null) return params.command.commandOwnerAllowFrom;
if (!params.isGroup) return params.effectiveAllowFrom;
return params.command?.groupOwnerAllowFrom === "none" ? [] : params.configuredAllowFrom;
}
function commandGroupAllowFrom(params) {
if (params.isGroup) return params.effectiveCommandGroupAllowFrom;
return params.command?.directGroupAllowFrom === "effective" ? params.effectiveCommandGroupAllowFrom : [];
}
function accessGroupMatchedEntry(params) {
const entry = params.accessGroupMatchedAllowFromEntry ?? params.subject.stableId;
return entry == null ? null : String(entry);
}
function appendAccessGroupMatchedEntry(params) {
return params.matchedEntry && params.allowlist.accessGroups.matched.length > 0 ? uniqueStrings([...params.entries, params.matchedEntry]) : params.entries;
}
/**
* Resolve sender, route, command, event, and activation gates for one inbound
* channel event.
*/
async function resolveChannelMessageIngress(params) {
const channelId = normalizeChannelId(params.channelId);
const adapter = createIdentityAdapter(params.identity);
const subject = createIdentitySubject(params.identity, params.subject);
const routeFacts = [...routeFactsFromDescriptors(params.route), ...params.routeFacts ?? []];
const storeAllowFrom = await readStoreAllowFrom({
...params,
channelId
});
const rawAllowFrom = normalizeStringEntries(params.allowFrom ?? []);
const rawStoreAllowFrom = normalizeStringEntries(storeAllowFrom);
const rawGroupAllowFrom = normalizeStringEntries(params.groupAllowFrom ?? []);
const normalizeEffective = (entries, context) => normalizeEffectiveEntries({
adapter,
accountId: params.accountId,
entries,
context
});
const [normalizedAllowFrom, normalizedStoreAllowFrom, normalizedGroupAllowFrom] = await Promise.all([
normalizeEffective(rawAllowFrom, "dm"),
normalizeEffective(rawStoreAllowFrom, "dm"),
normalizeEffective(rawGroupAllowFrom, "group")
]);
const accessGroupMembership = [...await resolveRuntimeAccessGroupMembershipFacts({
input: params,
channelId,
names: allReferencedAccessGroupNames([
rawAllowFrom,
rawGroupAllowFrom,
rawStoreAllowFrom,
params.command?.commandOwnerAllowFrom ?? [],
...routeFacts.map((route) => route.senderAllowFrom ?? [])
])
}), ...params.accessGroupMembership ?? []];
const baseEffective = resolveChannelIngressEffectiveAllowFromLists({
allowFrom: normalizedAllowFrom,
groupAllowFrom: normalizedGroupAllowFrom,
storeAllowFrom: normalizedStoreAllowFrom,
dmPolicy: params.policy.dmPolicy,
groupAllowFromFallbackToAllowFrom: params.policy.groupAllowFromFallbackToAllowFrom
});
const rawEffective = resolveChannelIngressEffectiveAllowFromLists({
allowFrom: rawAllowFrom,
groupAllowFrom: rawGroupAllowFrom,
storeAllowFrom: rawStoreAllowFrom,
dmPolicy: params.policy.dmPolicy,
groupAllowFromFallbackToAllowFrom: params.policy.groupAllowFromFallbackToAllowFrom
});
const rawCommandGroup = resolveChannelIngressEffectiveAllowFromLists({
allowFrom: rawAllowFrom,
groupAllowFrom: rawGroupAllowFrom,
dmPolicy: params.policy.dmPolicy,
groupAllowFromFallbackToAllowFrom: params.command?.commandGroupAllowFromFallbackToAllowFrom ?? params.policy.groupAllowFromFallbackToAllowFrom
});
const isGroup = params.conversation.kind !== "direct";
const policy = {
...params.policy,
...params.command !== void 0 ? { command: params.command } : {}
};
const state = await resolveChannelIngressState({
channelId,
accountId: params.accountId,
subject,
conversation: params.conversation,
adapter,
accessGroups: params.accessGroups,
accessGroupMembership,
routeFacts,
mentionFacts: params.mentionFacts,
event: params.event,
allowlists: {
dm: rawAllowFrom,
group: rawEffective.effectiveGroupAllowFrom,
pairingStore: rawStoreAllowFrom,
commandOwner: commandOwnerAllowFrom({
command: params.command,
isGroup,
configuredAllowFrom: rawAllowFrom,
effectiveAllowFrom: rawEffective.effectiveAllowFrom
}),
commandGroup: commandGroupAllowFrom({
command: params.command,
isGroup,
effectiveCommandGroupAllowFrom: rawCommandGroup.effectiveGroupAllowFrom
})
}
});
const ingress = decideChannelIngress(state, policy);
const matchedAccessGroupEntry = accessGroupMatchedEntry(params);
return {
state,
ingress,
senderAccess: projectSenderAccess({
ingress,
isGroup,
effectiveAllowFrom: appendAccessGroupMatchedEntry({
entries: baseEffective.effectiveAllowFrom,
allowlist: state.allowlists.dm,
matchedEntry: matchedAccessGroupEntry
}),
effectiveGroupAllowFrom: appendAccessGroupMatchedEntry({
entries: baseEffective.effectiveGroupAllowFrom,
allowlist: state.allowlists.group,
matchedEntry: matchedAccessGroupEntry
}),
providerMissingFallbackApplied: params.providerMissingFallbackApplied
}),
routeAccess: projectRouteAccess({
ingress,
route: params.route
}),
commandAccess: projectCommandAccess({
ingress,
policy
}),
activationAccess: projectActivationAccess({ ingress })
};
}
//#endregion
export { resolveChannelIngressState as a, resolveStableChannelMessageIngress as i, createChannelIngressResolver as n, defineStableChannelIngressIdentity as o, resolveChannelMessageIngress as r, decideChannelIngress as s, channelIngressRoutes as t };