mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 05:13:17 +08:00
Restrict delivery HTTP to loopback and fix grant races.
Require trusted loopback requests for proactive send, accept hyphenated WhatsApp group JIDs, and serialize access-grant mutations to avoid lost concurrent updates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
01924e5f7e
commit
cbbf373b6a
8 changed files with 411 additions and 240 deletions
|
|
@ -6,7 +6,7 @@
|
|||
export const ACCESS_GRANT_VERSION = 1;
|
||||
export const ACCESS_PENDING_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
export const ACCESS_GRANT_PHONE_MAX_LENGTH = 32;
|
||||
export const ACCESS_GRANT_GROUP_JID_PATTERN = /^\d{5,32}@g\.us$/;
|
||||
export const ACCESS_GRANT_GROUP_JID_PATTERN = /^\d{5,32}(?:-\d{1,32})?@g\.us$/;
|
||||
const AGENT_PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
|
||||
|
||||
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/;
|
||||
|
|
|
|||
|
|
@ -757,6 +757,41 @@ export class BotWorkspaceStore {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a mutator to the latest persisted grant inside the bot write queue.
|
||||
* Callers must not read-modify-write outside this method — concurrent inbound
|
||||
* (contact upsert, pending enqueue, admin approve) would otherwise lose updates.
|
||||
* @param {string} botId
|
||||
* @param {(current: object|null) => object|Promise<object>} mutator
|
||||
* @param {{ incarnation?: unknown }} [options]
|
||||
*/
|
||||
async mutateAccessGrant(botId, mutator, { incarnation } = {}) {
|
||||
const id = botIdOf(botId);
|
||||
if (typeof mutator !== 'function') {
|
||||
throw new TypeError('mutateAccessGrant requires a mutator function');
|
||||
}
|
||||
return this.#enqueue(id, async () => {
|
||||
if (!this.has(id)
|
||||
|| (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
|
||||
const error = new Error('找不到要修改的机器人。');
|
||||
error.code = 'workspace-bot-not-found';
|
||||
throw error;
|
||||
}
|
||||
const previous = this.#accessGrants[id] ?? null;
|
||||
const nextValue = await mutator(previous);
|
||||
const grant = validateAccessGrant(nextValue);
|
||||
this.#accessGrants[id] = grant;
|
||||
try {
|
||||
await this.#persist();
|
||||
} catch (error) {
|
||||
if (previous) this.#accessGrants[id] = previous;
|
||||
else delete this.#accessGrants[id];
|
||||
throw error;
|
||||
}
|
||||
return grant;
|
||||
});
|
||||
}
|
||||
|
||||
async bindWorkspaceSession(botId, value, {
|
||||
conversationKey,
|
||||
sessionId,
|
||||
|
|
|
|||
|
|
@ -100,6 +100,18 @@ export async function rememberWhatsappContact(workspaces, botId, grant, message,
|
|||
if (!phone && !lid) return grant;
|
||||
const pushName = message.contextSource?.()?.senderName;
|
||||
try {
|
||||
if (typeof workspaces.mutateAccessGrant === 'function') {
|
||||
return await workspaces.mutateAccessGrant(botId, (current) => {
|
||||
const base = current ?? grant;
|
||||
return upsertAccessContact(base, {
|
||||
phone,
|
||||
lid,
|
||||
pushName,
|
||||
scene: message.kind === 'group' ? 'group' : 'direct',
|
||||
groupJid: message.kind === 'group' ? message.conversationId : undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
const next = upsertAccessContact(grant, {
|
||||
phone,
|
||||
lid,
|
||||
|
|
@ -144,12 +156,26 @@ export async function tryHandleWhatsappApprovalReply({
|
|||
if (!pending || pending.status !== 'pending') return false;
|
||||
|
||||
try {
|
||||
const { grant: next, pending: resolved } = resolveAccessPending(grant, {
|
||||
pendingId: pending.id,
|
||||
action: intent,
|
||||
resolvedByPhone: phone,
|
||||
});
|
||||
await workspaces.setAccessGrant(botId, next);
|
||||
let resolved = pending;
|
||||
if (typeof workspaces.mutateAccessGrant === 'function') {
|
||||
await workspaces.mutateAccessGrant(botId, (current) => {
|
||||
const result = resolveAccessPending(current ?? grant, {
|
||||
pendingId: pending.id,
|
||||
action: intent,
|
||||
resolvedByPhone: phone,
|
||||
});
|
||||
resolved = result.pending;
|
||||
return result.grant;
|
||||
});
|
||||
} else {
|
||||
const result = resolveAccessPending(grant, {
|
||||
pendingId: pending.id,
|
||||
action: intent,
|
||||
resolvedByPhone: phone,
|
||||
});
|
||||
resolved = result.pending;
|
||||
await workspaces.setAccessGrant(botId, result.grant);
|
||||
}
|
||||
await sendText(
|
||||
{ jid: message.replyTarget?.jid ?? `${phone}@s.whatsapp.net` },
|
||||
intent === 'approve' ? ACCESS_GRANT_COPY.adminApproved : ACCESS_GRANT_COPY.adminDenied,
|
||||
|
|
@ -264,16 +290,37 @@ export async function gateWhatsappInbound({
|
|||
return { allowed: false, reason: 'group-unaddressed', grant: current };
|
||||
}
|
||||
|
||||
const { grant: withPending, pending, created } = enqueueAccessPending(current, {
|
||||
kind: scene,
|
||||
groupJid: scene === 'group' ? message.conversationId : undefined,
|
||||
phone: phone ?? '',
|
||||
lid: lid ?? undefined,
|
||||
pushName: message.contextSource?.()?.senderName,
|
||||
requestText: message.content,
|
||||
});
|
||||
current = withPending;
|
||||
await workspaces.setAccessGrant(botId, current);
|
||||
let pending;
|
||||
let created = false;
|
||||
if (typeof workspaces.mutateAccessGrant === 'function') {
|
||||
let enqueued = null;
|
||||
current = await workspaces.mutateAccessGrant(botId, (latest) => {
|
||||
enqueued = enqueueAccessPending(latest ?? current, {
|
||||
kind: scene,
|
||||
groupJid: scene === 'group' ? message.conversationId : undefined,
|
||||
phone: phone ?? '',
|
||||
lid: lid ?? undefined,
|
||||
pushName: message.contextSource?.()?.senderName,
|
||||
requestText: message.content,
|
||||
});
|
||||
return enqueued.grant;
|
||||
});
|
||||
pending = enqueued.pending;
|
||||
created = enqueued.created;
|
||||
} else {
|
||||
const enqueued = enqueueAccessPending(current, {
|
||||
kind: scene,
|
||||
groupJid: scene === 'group' ? message.conversationId : undefined,
|
||||
phone: phone ?? '',
|
||||
lid: lid ?? undefined,
|
||||
pushName: message.contextSource?.()?.senderName,
|
||||
requestText: message.content,
|
||||
});
|
||||
current = enqueued.grant;
|
||||
pending = enqueued.pending;
|
||||
created = enqueued.created;
|
||||
await workspaces.setAccessGrant(botId, current);
|
||||
}
|
||||
|
||||
const ack = !phone
|
||||
? ACCESS_GRANT_COPY.pendingUnresolved
|
||||
|
|
@ -310,8 +357,14 @@ export async function gateWhatsappInbound({
|
|||
}
|
||||
}
|
||||
if (refs.length > 0) {
|
||||
current = attachPendingNotifyRefs(current, pending.id, refs);
|
||||
await workspaces.setAccessGrant(botId, current);
|
||||
if (typeof workspaces.mutateAccessGrant === 'function') {
|
||||
current = await workspaces.mutateAccessGrant(botId, (latest) => (
|
||||
attachPendingNotifyRefs(latest ?? current, pending.id, refs)
|
||||
));
|
||||
} else {
|
||||
current = attachPendingNotifyRefs(current, pending.id, refs);
|
||||
await workspaces.setAccessGrant(botId, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue