mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 01:53:21 +08:00
Add WhatsApp graded phone ACL (4.9.1-ops.4).
Replace allowlist-only gating with phone-scoped grants: global admins, DM members, per-group admins/members, pending approval via quote YES/NO or settings UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e5e1e6573a
commit
9f97c44031
15 changed files with 3146 additions and 327 deletions
78
plugin-src/host/channels/shared/access-grant-rpc.mjs
Normal file
78
plugin-src/host/channels/shared/access-grant-rpc.mjs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { validateAccessGrant, normalizeAccessGrant } from '../../../../src/channels/shared/access-grant.mjs';
|
||||
|
||||
export const SET_ACCESS_GRANT_ENDPOINT = 'bot.access-grant.set';
|
||||
export const RESOLVE_ACCESS_PENDING_ENDPOINT = 'bot.access-pending.resolve';
|
||||
|
||||
export function validAccessGrantPayload(payload) {
|
||||
try {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)
|
||||
|| !Object.hasOwn(payload, 'botId') || !Object.hasOwn(payload, 'grant')
|
||||
|| typeof payload.botId !== 'string'
|
||||
|| !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) return false;
|
||||
validateAccessGrant(payload.grant);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function validAccessPendingResolvePayload(payload) {
|
||||
try {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return false;
|
||||
const keys = Reflect.ownKeys(payload);
|
||||
if (keys.length !== 4
|
||||
|| !Object.hasOwn(payload, 'botId')
|
||||
|| !Object.hasOwn(payload, 'pendingId')
|
||||
|| !Object.hasOwn(payload, 'action')
|
||||
|| !Object.hasOwn(payload, 'resolvedByPhone')) return false;
|
||||
if (typeof payload.botId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof payload.pendingId !== 'string' || !/^[A-Za-z0-9_-]{6,64}$/.test(payload.pendingId)) {
|
||||
return false;
|
||||
}
|
||||
if (payload.action !== 'approve' && payload.action !== 'deny') return false;
|
||||
if (typeof payload.resolvedByPhone !== 'string' || !payload.resolvedByPhone.trim()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function publicAccessGrant(grant) {
|
||||
const normalized = normalizeAccessGrant(grant);
|
||||
if (!normalized) return null;
|
||||
return {
|
||||
version: normalized.version,
|
||||
globalAdmins: [...normalized.globalAdmins],
|
||||
directMembers: normalized.directMembers.map((m) => ({ ...m })),
|
||||
groups: Object.fromEntries(Object.entries(normalized.groups).map(([jid, group]) => [jid, {
|
||||
...(group.title ? { title: group.title } : {}),
|
||||
admins: [...group.admins],
|
||||
members: group.members.map((m) => ({ ...m })),
|
||||
}])),
|
||||
pending: normalized.pending
|
||||
.filter((entry) => entry.status === 'pending')
|
||||
.map((entry) => ({
|
||||
id: entry.id,
|
||||
kind: entry.kind,
|
||||
...(entry.groupJid ? { groupJid: entry.groupJid } : {}),
|
||||
phone: entry.phone,
|
||||
...(entry.lid ? { lid: entry.lid } : {}),
|
||||
...(entry.pushName ? { pushName: entry.pushName } : {}),
|
||||
...(entry.requestText ? { requestText: entry.requestText } : {}),
|
||||
createdAt: entry.createdAt,
|
||||
unresolved: entry.unresolved === true,
|
||||
})),
|
||||
contacts: normalized.contacts.slice(0, 100).map((contact) => ({
|
||||
...(contact.phone ? { phone: contact.phone } : {}),
|
||||
lids: [...contact.lids],
|
||||
...(contact.pushName ? { pushName: contact.pushName } : {}),
|
||||
lastSeenAt: contact.lastSeenAt,
|
||||
scenes: [...contact.scenes],
|
||||
...(contact.groupJids ? { groupJids: [...contact.groupJids] } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
@ -123,6 +123,8 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|||
channel: 'whatsapp', config: botConfig, equals: whatsappAccessPolicyIdsEqual,
|
||||
}),
|
||||
groupSessionScope: { getScope: () => workspaces.groupSessionScopeFor(botId) },
|
||||
workspaces,
|
||||
botId,
|
||||
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
||||
connectTimeoutMs: config.connectTimeoutMs ?? 30_000,
|
||||
createSession,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ import QRCode from 'qrcode';
|
|||
|
||||
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||
import {
|
||||
RESOLVE_ACCESS_PENDING_ENDPOINT,
|
||||
SET_ACCESS_GRANT_ENDPOINT,
|
||||
validAccessGrantPayload,
|
||||
validAccessPendingResolvePayload,
|
||||
} from '../shared/access-grant-rpc.mjs';
|
||||
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||
|
|
@ -17,6 +23,8 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|||
reconnectBot: 'bot.reconnect',
|
||||
deleteBot: 'bot.delete',
|
||||
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
||||
setAccessGrant: SET_ACCESS_GRANT_ENDPOINT,
|
||||
resolveAccessPending: RESOLVE_ACCESS_PENDING_ENDPOINT,
|
||||
setGroupSessionScope: SET_GROUP_SESSION_SCOPE_ENDPOINT,
|
||||
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||
|
|
@ -60,6 +68,14 @@ function payloadFailure(endpoint, payload) {
|
|||
return validAccessPolicyPayload(payload)
|
||||
? null : '请提交有效的访问设置。';
|
||||
}
|
||||
if (endpoint === WHATSAPP_ENDPOINTS.setAccessGrant) {
|
||||
return validAccessGrantPayload(payload)
|
||||
? null : '请提交有效的分级访问授权。';
|
||||
}
|
||||
if (endpoint === WHATSAPP_ENDPOINTS.resolveAccessPending) {
|
||||
return validAccessPendingResolvePayload(payload)
|
||||
? null : '请提交有效的审批请求。';
|
||||
}
|
||||
if (endpoint === WHATSAPP_ENDPOINTS.setGroupSessionScope) {
|
||||
return validGroupSessionScopePayload(payload)
|
||||
? null : '请提交有效的群会话策略。';
|
||||
|
|
@ -196,6 +212,24 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|||
payload.policy,
|
||||
(status) => publicStatus(status, cachedEncode),
|
||||
);
|
||||
} else if (endpoint === WHATSAPP_ENDPOINTS.setAccessGrant) {
|
||||
if (typeof controller.updateAccessGrant !== 'function') throw new Error('Access grant update is unavailable');
|
||||
value = await controller.updateAccessGrant(
|
||||
payload.botId,
|
||||
payload.grant,
|
||||
(status) => publicStatus(status, cachedEncode),
|
||||
);
|
||||
} else if (endpoint === WHATSAPP_ENDPOINTS.resolveAccessPending) {
|
||||
if (typeof controller.resolveAccessPending !== 'function') throw new Error('Access pending resolve is unavailable');
|
||||
value = await controller.resolveAccessPending(
|
||||
payload.botId,
|
||||
{
|
||||
pendingId: payload.pendingId,
|
||||
action: payload.action,
|
||||
resolvedByPhone: payload.resolvedByPhone,
|
||||
},
|
||||
(status) => publicStatus(status, cachedEncode),
|
||||
);
|
||||
} else if (endpoint === WHATSAPP_ENDPOINTS.setGroupSessionScope) {
|
||||
if (typeof controller.updateGroupSessionScope !== 'function') throw new Error('Group session scope update is unavailable');
|
||||
value = await controller.updateGroupSessionScope(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue