@excitedjs/feishu-transport 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +39 -0
  2. package/dist/contract/access-store.d.ts +23 -0
  3. package/dist/contract/access-store.d.ts.map +1 -0
  4. package/dist/contract/access-store.js +16 -0
  5. package/dist/contract/access-store.js.map +1 -0
  6. package/dist/contract/outbound.d.ts +39 -0
  7. package/dist/contract/outbound.d.ts.map +1 -0
  8. package/dist/contract/outbound.js +16 -0
  9. package/dist/contract/outbound.js.map +1 -0
  10. package/dist/contract/types.d.ts +86 -0
  11. package/dist/contract/types.d.ts.map +1 -0
  12. package/dist/contract/types.js +10 -0
  13. package/dist/contract/types.js.map +1 -0
  14. package/dist/index.d.ts +29 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +31 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/json.d.ts +10 -0
  19. package/dist/json.d.ts.map +1 -0
  20. package/dist/json.js +14 -0
  21. package/dist/json.js.map +1 -0
  22. package/dist/parse/comment.d.ts +41 -0
  23. package/dist/parse/comment.d.ts.map +1 -0
  24. package/dist/parse/comment.js +51 -0
  25. package/dist/parse/comment.js.map +1 -0
  26. package/dist/parse/content.d.ts +42 -0
  27. package/dist/parse/content.d.ts.map +1 -0
  28. package/dist/parse/content.js +208 -0
  29. package/dist/parse/content.js.map +1 -0
  30. package/dist/policy/gate.d.ts +105 -0
  31. package/dist/policy/gate.d.ts.map +1 -0
  32. package/dist/policy/gate.js +276 -0
  33. package/dist/policy/gate.js.map +1 -0
  34. package/dist/policy/pairing.d.ts +12 -0
  35. package/dist/policy/pairing.d.ts.map +1 -0
  36. package/dist/policy/pairing.js +15 -0
  37. package/dist/policy/pairing.js.map +1 -0
  38. package/dist/render/render.d.ts +186 -0
  39. package/dist/render/render.d.ts.map +1 -0
  40. package/dist/render/render.js +630 -0
  41. package/dist/render/render.js.map +1 -0
  42. package/dist/transport/connection.d.ts +25 -0
  43. package/dist/transport/connection.d.ts.map +1 -0
  44. package/dist/transport/connection.js +42 -0
  45. package/dist/transport/connection.js.map +1 -0
  46. package/dist/transport/feishu.d.ts +222 -0
  47. package/dist/transport/feishu.d.ts.map +1 -0
  48. package/dist/transport/feishu.js +431 -0
  49. package/dist/transport/feishu.js.map +1 -0
  50. package/package.json +39 -0
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Parsing inbound Feishu message content.
3
+ *
4
+ * Feishu delivers `message.content` as a JSON-encoded string whose shape
5
+ * depends on `message_type`. This module turns that into the plain text the
6
+ * channel forwards to the engine. Attachment message types (image, file) are
7
+ * summarized as a short text marker — the channel forwards text, not binaries.
8
+ *
9
+ * Ported verbatim from claudemux's `feishu-channel/src/content.ts` (the source
10
+ * of truth — it carries the `interactive`-card parse dreamux's drifted copy had
11
+ * lost); only the `./types` import was repointed to `../contract/types`.
12
+ */
13
+ /**
14
+ * Parse one inbound Feishu message into forwardable text. Never throws —
15
+ * malformed content falls back to a best-effort string so a weird message
16
+ * still reaches the engine.
17
+ */
18
+ export function parseInbound(message) {
19
+ const type = message.message_type ?? 'unknown';
20
+ let parsed;
21
+ try {
22
+ parsed = JSON.parse(message.content ?? '');
23
+ }
24
+ catch {
25
+ return { text: message.content ?? '(unparseable message)' };
26
+ }
27
+ const content = (parsed && typeof parsed === 'object' ? parsed : {});
28
+ switch (type) {
29
+ case 'text': {
30
+ const text = typeof content.text === 'string' ? content.text : '';
31
+ return { text: applyMentions(text, message.mentions) };
32
+ }
33
+ case 'post':
34
+ return { text: extractPostText(content) };
35
+ case 'image':
36
+ return { text: '(image)' };
37
+ case 'file': {
38
+ const fileName = typeof content.file_name === 'string' ? content.file_name : 'unknown';
39
+ return { text: `(file: ${fileName})` };
40
+ }
41
+ case 'interactive':
42
+ return { text: extractInteractiveText(content) };
43
+ default:
44
+ return { text: `(${type} message)` };
45
+ }
46
+ }
47
+ /**
48
+ * Feishu WebSocket events for interactive cards wrap the real v2 card JSON as a
49
+ * JSON-encoded string under `user_dsl`. Unwrap it so the extractor below always
50
+ * sees the card schema directly.
51
+ */
52
+ function unwrapUserDsl(card) {
53
+ const dsl = card.user_dsl;
54
+ if (typeof dsl !== 'string')
55
+ return card;
56
+ try {
57
+ const inner = JSON.parse(dsl);
58
+ if (inner && typeof inner === 'object' && !Array.isArray(inner)) {
59
+ return inner;
60
+ }
61
+ }
62
+ catch {
63
+ // fall through
64
+ }
65
+ return card;
66
+ }
67
+ /**
68
+ * Extract plain text from a v2 interactive card content object.
69
+ * Handles feishu-channel cards (tag: markdown) and Dbotmux / other bots'
70
+ * cards (tag: div with text.content, tag: column_set, etc.).
71
+ */
72
+ function extractInteractiveText(card) {
73
+ const c = unwrapUserDsl(card);
74
+ const parts = [];
75
+ const header = c.header;
76
+ if (header && typeof header === 'object') {
77
+ const title = header.title;
78
+ if (title && typeof title === 'object') {
79
+ const tc = title.content;
80
+ if (typeof tc === 'string' && tc.trim())
81
+ parts.push(tc);
82
+ }
83
+ }
84
+ const body = c.body;
85
+ const elements = body && typeof body === 'object'
86
+ ? body.elements
87
+ : c.elements;
88
+ if (Array.isArray(elements)) {
89
+ for (const el of elements)
90
+ extractCardElementText(el, parts);
91
+ }
92
+ return parts.join('\n') || '(interactive card)';
93
+ }
94
+ /** Recursively extract readable text from a v2 card element. */
95
+ function extractCardElementText(el, parts) {
96
+ if (!el || typeof el !== 'object' || Array.isArray(el))
97
+ return;
98
+ const e = el;
99
+ const tag = e.tag;
100
+ if (tag === 'markdown' || tag === 'plain_text' || tag === 'div') {
101
+ // `content` is a direct string in feishu-channel cards;
102
+ // `text.content` is used when the text is a nested object (other bots).
103
+ const textObj = e.text;
104
+ const text = textObj && typeof textObj === 'object'
105
+ ? textObj.content
106
+ : e.content;
107
+ if (typeof text === 'string' && text.trim())
108
+ parts.push(text);
109
+ // div.fields[] — lark_md cells in field-layout cards from other bots.
110
+ if (Array.isArray(e.fields)) {
111
+ for (const f of e.fields) {
112
+ if (!f || typeof f !== 'object')
113
+ continue;
114
+ const fo = f;
115
+ const ft = fo.text && typeof fo.text === 'object'
116
+ ? fo.text.content
117
+ : fo.content;
118
+ if (typeof ft === 'string' && ft.trim())
119
+ parts.push(ft);
120
+ }
121
+ }
122
+ }
123
+ // column_set → columns[].elements[]
124
+ if (Array.isArray(e.columns)) {
125
+ for (const col of e.columns) {
126
+ if (!col || typeof col !== 'object')
127
+ continue;
128
+ const co = col;
129
+ if (Array.isArray(co.elements)) {
130
+ for (const child of co.elements)
131
+ extractCardElementText(child, parts);
132
+ }
133
+ }
134
+ }
135
+ // Generic child elements (action blocks, nested containers)
136
+ if (Array.isArray(e.elements)) {
137
+ for (const child of e.elements)
138
+ extractCardElementText(child, parts);
139
+ }
140
+ }
141
+ /**
142
+ * Replace Feishu's `@_user_N` placeholders in text with the mentioned display
143
+ * names, so the forwarded message reads naturally.
144
+ */
145
+ export function applyMentions(text, mentions) {
146
+ if (!mentions)
147
+ return text;
148
+ let out = text;
149
+ for (const m of mentions) {
150
+ if (m.key && m.name) {
151
+ out = out.split(m.key).join(`@${m.name}`);
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+ /**
157
+ * Flatten a Feishu rich-text "post" payload into plain text. A post is
158
+ * locale-wrapped (`{ zh_cn: { title, content } }`) and its body is an array of
159
+ * paragraphs, each an array of tagged inline elements.
160
+ */
161
+ export function extractPostText(content) {
162
+ const post = pickPostLocale(content);
163
+ const lines = [];
164
+ if (typeof post.title === 'string' && post.title.length > 0) {
165
+ lines.push(post.title);
166
+ }
167
+ const body = post.content;
168
+ if (Array.isArray(body)) {
169
+ for (const paragraph of body) {
170
+ if (!Array.isArray(paragraph))
171
+ continue;
172
+ lines.push(paragraph.map(renderPostElement).join(''));
173
+ }
174
+ }
175
+ return lines.join('\n');
176
+ }
177
+ /** Pick the first present locale block of a post, falling back to the raw object. */
178
+ function pickPostLocale(content) {
179
+ for (const locale of ['zh_cn', 'en_us', 'ja_jp']) {
180
+ const block = content[locale];
181
+ if (block && typeof block === 'object')
182
+ return block;
183
+ }
184
+ return content;
185
+ }
186
+ /** Render one inline post element to text. */
187
+ function renderPostElement(el) {
188
+ if (!el || typeof el !== 'object')
189
+ return '';
190
+ const e = el;
191
+ switch (e.tag) {
192
+ case 'text':
193
+ return typeof e.text === 'string' ? e.text : '';
194
+ case 'a':
195
+ return typeof e.text === 'string'
196
+ ? e.text
197
+ : typeof e.href === 'string'
198
+ ? e.href
199
+ : '';
200
+ case 'at':
201
+ return `@${typeof e.user_name === 'string' ? e.user_name : ''}`;
202
+ case 'img':
203
+ return '(image)';
204
+ default:
205
+ return '';
206
+ }
207
+ }
208
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/parse/content.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Access control — the security-critical gate every inbound message passes
3
+ * through before it can reach the engine session.
4
+ *
5
+ * `gate` is a pure function: it takes the current access state plus the
6
+ * message's identity fields and returns a decision. It never reads the clock,
7
+ * never touches disk, and never mutates its input — the caller injects `now`
8
+ * and a candidate `newCode`, and persists `result.access` when `result.changed`
9
+ * is true. That makes every branch here exhaustively unit-testable.
10
+ *
11
+ * Ported verbatim from claudemux's `feishu-channel/src/access.ts` (the source
12
+ * of truth); only the `./types` import was repointed to `../contract/types`.
13
+ */
14
+ import type { Access, Mention } from '../contract/types.js';
15
+ /** Cap on simultaneously-pending pairing requests — direct and group share it. */
16
+ export declare const MAX_PENDING = 10;
17
+ /** Cap on pairing-code replies sent to one un-paired sender. */
18
+ export declare const MAX_PAIRING_REPLIES = 2;
19
+ /** How long a pairing code stays valid, in milliseconds. */
20
+ export declare const PAIRING_TTL_MS: number;
21
+ export interface GateInput {
22
+ /** open_id of the message sender. */
23
+ senderId: string;
24
+ /**
25
+ * Feishu sender_type. Feishu uses `'bot'` for cross-bot card messages and
26
+ * `'app'` for custom-bot messages in some scenarios; `'user'` for humans.
27
+ * Absent when the field is missing from the raw payload.
28
+ */
29
+ senderType?: string;
30
+ /** chat_id the message arrived in. */
31
+ chatId: string;
32
+ /** Feishu chat_type — `p2p` or `group`. */
33
+ chatType: string;
34
+ /** Current access-control state. */
35
+ access: Access;
36
+ /** Injected clock (epoch millis) — keeps `gate` pure. */
37
+ now: number;
38
+ /** A fresh pairing code, used only if `gate` starts a new pairing. */
39
+ newCode: string;
40
+ /** @-mentions carried by the message, for group mention-gating. */
41
+ mentions?: Mention[];
42
+ /** open_id of the bot itself, for group mention-gating. */
43
+ botOpenId?: string;
44
+ /**
45
+ * open_ids of peer bots known via /introduce in this specific chat. Populated
46
+ * by the caller for group messages only; `undefined` for direct messages.
47
+ * Entries arise from two sources, both scoped to this chatId:
48
+ * - an authorized human sender ran /introduce (trust via the gate that
49
+ * governed that delivery), or
50
+ * - a bot sender broadcast /introduce in an authorized group (ambient
51
+ * self-recording; `isBotSenderType` and `isGroupAuthorized` are the guards).
52
+ */
53
+ observedBotIds?: ReadonlySet<string>;
54
+ }
55
+ export type GateResult = {
56
+ action: 'deliver';
57
+ access: Access;
58
+ changed: boolean;
59
+ } | {
60
+ action: 'drop';
61
+ access: Access;
62
+ changed: boolean;
63
+ reason: string;
64
+ } | {
65
+ action: 'pair';
66
+ access: Access;
67
+ changed: boolean;
68
+ code: string;
69
+ isResend: boolean;
70
+ };
71
+ /**
72
+ * Decide what to do with one inbound message. Returns the (possibly updated)
73
+ * access state in `access` and whether it differs from the input in `changed`.
74
+ */
75
+ export declare function gate(input: GateInput): GateResult;
76
+ /** True when one of `mentions` resolves to the bot's own open_id. */
77
+ export declare function isBotMentioned(mentions: Mention[] | undefined, botOpenId: string | undefined): boolean;
78
+ /**
79
+ * Drop pairing entries whose `expiresAt` is at or before `now`. Returns the
80
+ * same object reference when nothing expired, so callers can cheaply skip a
81
+ * disk write via the `changed` flag.
82
+ */
83
+ export declare function pruneExpiredPending(access: Access, now: number): {
84
+ access: Access;
85
+ changed: boolean;
86
+ };
87
+ /**
88
+ * True when `senderType` identifies a Feishu bot or app.
89
+ * Feishu uses `'bot'` for cross-bot messages and `'app'` for custom-bot
90
+ * messages in some event contexts; both are non-human senders.
91
+ */
92
+ export declare function isBotSenderType(senderType: string | undefined): boolean;
93
+ /**
94
+ * True when the given group is "authorized" — i.e. the channel is actively
95
+ * serving it and ambient side effects (like /introduce recording) are appropriate.
96
+ *
97
+ * - `block` → never authorized; the bot ignores all groups.
98
+ * - `follow-user` → always authorized; any group can receive messages.
99
+ * - `allowlist` → authorized only when the group has been paired and is
100
+ * present in `access.groups`. When `senderId` is provided,
101
+ * also checks that the sender passes the group's `allowFrom`
102
+ * filter (empty allowFrom = no restriction).
103
+ */
104
+ export declare function isGroupAuthorized(access: Access, chatId: string, senderId?: string): boolean;
105
+ //# sourceMappingURL=gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/policy/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAgB,MAAM,sBAAsB,CAAA;AAEzE,kFAAkF;AAClF,eAAO,MAAM,WAAW,KAAK,CAAA;AAC7B,gEAAgE;AAChE,eAAO,MAAM,mBAAmB,IAAI,CAAA;AACpC,4DAA4D;AAC5D,eAAO,MAAM,cAAc,QAAiB,CAAA;AAE5C,MAAM,WAAW,SAAS;IACxB,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAA;IACd,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAA;IAChB,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAA;IACX,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAA;IACf,mEAAmE;IACnE,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACrC;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACvD;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAA;AAEzF;;;GAGG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAkBjD;AA+LD,qEAAqE;AACrE,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,OAAO,EAAE,GAAG,SAAS,EAC/B,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAGT;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GACV;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAWtC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEvE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAS5F"}
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Access control — the security-critical gate every inbound message passes
3
+ * through before it can reach the engine session.
4
+ *
5
+ * `gate` is a pure function: it takes the current access state plus the
6
+ * message's identity fields and returns a decision. It never reads the clock,
7
+ * never touches disk, and never mutates its input — the caller injects `now`
8
+ * and a candidate `newCode`, and persists `result.access` when `result.changed`
9
+ * is true. That makes every branch here exhaustively unit-testable.
10
+ *
11
+ * Ported verbatim from claudemux's `feishu-channel/src/access.ts` (the source
12
+ * of truth); only the `./types` import was repointed to `../contract/types`.
13
+ */
14
+ /** Cap on simultaneously-pending pairing requests — direct and group share it. */
15
+ export const MAX_PENDING = 10;
16
+ /** Cap on pairing-code replies sent to one un-paired sender. */
17
+ export const MAX_PAIRING_REPLIES = 2;
18
+ /** How long a pairing code stays valid, in milliseconds. */
19
+ export const PAIRING_TTL_MS = 60 * 60 * 1000;
20
+ /**
21
+ * Decide what to do with one inbound message. Returns the (possibly updated)
22
+ * access state in `access` and whether it differs from the input in `changed`.
23
+ */
24
+ export function gate(input) {
25
+ const pruned = pruneExpiredPending(input.access, input.now);
26
+ if (!input.senderId) {
27
+ return { action: 'drop', access: pruned.access, changed: pruned.changed, reason: 'missing sender id' };
28
+ }
29
+ if (input.chatType === 'p2p') {
30
+ return gateDirect(input, pruned.access, pruned.changed);
31
+ }
32
+ if (input.chatType === 'group') {
33
+ return gateGroup(input, pruned.access, pruned.changed);
34
+ }
35
+ return {
36
+ action: 'drop',
37
+ access: pruned.access,
38
+ changed: pruned.changed,
39
+ reason: `unsupported chat type: ${input.chatType}`,
40
+ };
41
+ }
42
+ /** Decide a direct (1:1) message. */
43
+ function gateDirect(input, access, changed) {
44
+ if (access.dmPolicy === 'disabled') {
45
+ return { action: 'drop', access, changed, reason: 'direct messages disabled' };
46
+ }
47
+ if (access.allowFrom.includes(input.senderId)) {
48
+ return { action: 'deliver', access, changed };
49
+ }
50
+ if (access.dmPolicy === 'allowlist') {
51
+ return { action: 'drop', access, changed, reason: 'sender not on allowlist' };
52
+ }
53
+ // dmPolicy === 'pairing' — an unknown sender starts (or repeats) a pairing.
54
+ // The `kind` guard matters: a group-pairing entry also carries a senderId
55
+ // (its triggerer), so without it a group triggerer's later DM would match
56
+ // that entry and be answered with the group's code.
57
+ for (const [code, entry] of Object.entries(access.pending)) {
58
+ if (entry.kind !== 'dm' || entry.senderId !== input.senderId)
59
+ continue;
60
+ if (entry.replies >= MAX_PAIRING_REPLIES) {
61
+ return { action: 'drop', access, changed, reason: 'pairing reply cap reached' };
62
+ }
63
+ const nextAccess = {
64
+ ...access,
65
+ pending: { ...access.pending, [code]: { ...entry, replies: entry.replies + 1 } },
66
+ };
67
+ return { action: 'pair', access: nextAccess, changed: true, code, isResend: true };
68
+ }
69
+ if (Object.keys(access.pending).length >= MAX_PENDING) {
70
+ return { action: 'drop', access, changed, reason: 'too many pending pairings' };
71
+ }
72
+ const entry = {
73
+ kind: 'dm',
74
+ senderId: input.senderId,
75
+ chatId: input.chatId,
76
+ createdAt: input.now,
77
+ expiresAt: input.now + PAIRING_TTL_MS,
78
+ replies: 1,
79
+ };
80
+ const nextAccess = {
81
+ ...access,
82
+ pending: { ...access.pending, [input.newCode]: entry },
83
+ };
84
+ return { action: 'pair', access: nextAccess, changed: true, code: input.newCode, isResend: false };
85
+ }
86
+ /**
87
+ * Decide a group message according to `access.groupPolicy` — the switch that
88
+ * selects one of three group-access modes. See `GroupPolicy` in `types.ts`.
89
+ */
90
+ function gateGroup(input, access, changed) {
91
+ if (access.groupPolicy === 'block') {
92
+ return {
93
+ action: 'drop',
94
+ access,
95
+ changed,
96
+ reason: 'group messages are blocked (groupPolicy: block)',
97
+ };
98
+ }
99
+ if (access.groupPolicy === 'follow-user') {
100
+ return gateGroupFollowUser(input, access, changed);
101
+ }
102
+ // 'allowlist' — a group is authorized as a unit, by pairing (decision feishu-channel-group-pairing).
103
+ return gateGroupAllowlist(input, access, changed);
104
+ }
105
+ /**
106
+ * Decide a group message under the `follow-user` policy: the group itself
107
+ * needs no authorization — the sender does. A message is delivered when the
108
+ * bot is @-mentioned (the deliberate "engage the bot" signal, without which
109
+ * the bot would react to every message in the group) AND the sender's open_id
110
+ * is either on the top-level `allowFrom` allowlist (the same allowlist that
111
+ * authorizes direct messages) OR is a peer bot known via /introduce in this
112
+ * chat. A non-mention message, or a mention from an unrecognized sender, is
113
+ * dropped; no pairing code is posted into a group.
114
+ *
115
+ * The observed-bot path is safe: entries are per-chatId and arise from two
116
+ * guarded sources — an authorized human /introduce delivery, or a bot that
117
+ * broadcast /introduce in an authorized group (ambient path, guarded by
118
+ * `isBotSenderType` + `isGroupAuthorized`). Both scope trust to this chat.
119
+ */
120
+ function gateGroupFollowUser(input, access, changed) {
121
+ if (input.botOpenId === undefined) {
122
+ return {
123
+ action: 'drop',
124
+ access,
125
+ changed,
126
+ reason: 'group message requires an @-mention but the bot open_id is unknown',
127
+ };
128
+ }
129
+ if (!isBotMentioned(input.mentions, input.botOpenId)) {
130
+ return { action: 'drop', access, changed, reason: 'bot not mentioned' };
131
+ }
132
+ const onAllowlist = access.allowFrom.includes(input.senderId);
133
+ const isIntroducedBot = isBotSenderType(input.senderType) && (input.observedBotIds?.has(input.senderId) ?? false);
134
+ if (!onAllowlist && !isIntroducedBot) {
135
+ return { action: 'drop', access, changed, reason: 'sender not on allowlist' };
136
+ }
137
+ return { action: 'deliver', access, changed };
138
+ }
139
+ /**
140
+ * Decide a group message under the `allowlist` policy — a group is authorized
141
+ * as a unit (decision feishu-channel-group-pairing). A configured group is gated by its own
142
+ * `requireMention` / `allowFrom` entry; an unconfigured group is brought in by
143
+ * pairing.
144
+ */
145
+ function gateGroupAllowlist(input, access, changed) {
146
+ const policy = access.groups[input.chatId];
147
+ if (!policy) {
148
+ return gateUnconfiguredGroup(input, access, changed);
149
+ }
150
+ if (policy.allowFrom.length > 0 && !policy.allowFrom.includes(input.senderId)) {
151
+ return { action: 'drop', access, changed, reason: 'sender not allowed in this group' };
152
+ }
153
+ if (policy.requireMention) {
154
+ // An unknown bot open_id cannot match any mention, so a mention-gated
155
+ // group would drop every message. Report that as its own reason rather
156
+ // than the misleading "bot not mentioned".
157
+ if (input.botOpenId === undefined) {
158
+ return {
159
+ action: 'drop',
160
+ access,
161
+ changed,
162
+ reason: 'group requires an @-mention but the bot open_id is unknown',
163
+ };
164
+ }
165
+ if (!isBotMentioned(input.mentions, input.botOpenId)) {
166
+ return { action: 'drop', access, changed, reason: 'bot not mentioned' };
167
+ }
168
+ }
169
+ return { action: 'deliver', access, changed };
170
+ }
171
+ /**
172
+ * Decide a message from a group that is not yet in `access.groups`.
173
+ *
174
+ * A group joins `access.groups` the same way an unknown direct sender joins
175
+ * the allowlist — by pairing. But a group pairing is started only by a
176
+ * deliberate @-mention of the bot, the group equivalent of choosing to open a
177
+ * 1:1 chat: that keeps the bot from posting a pairing code in every group it
178
+ * was incidentally added to, and bounds it to one code per group rather than
179
+ * one per group message. A non-mention message is dropped silently.
180
+ *
181
+ * Only one pairing runs per group at a time: while a `group` entry for this
182
+ * chat_id is pending, further mentions are dropped rather than posting a
183
+ * second code. The entry expires via `pruneExpiredPending`, so an un-approved
184
+ * pairing reopens on the next mention after its TTL.
185
+ */
186
+ function gateUnconfiguredGroup(input, access, changed) {
187
+ if (input.botOpenId === undefined) {
188
+ return {
189
+ action: 'drop',
190
+ access,
191
+ changed,
192
+ reason: 'unconfigured group; bot open_id is unknown, so a mention cannot be detected',
193
+ };
194
+ }
195
+ if (!isBotMentioned(input.mentions, input.botOpenId)) {
196
+ return { action: 'drop', access, changed, reason: 'unconfigured group; bot not mentioned' };
197
+ }
198
+ for (const entry of Object.values(access.pending)) {
199
+ if (entry.kind === 'group' && entry.chatId === input.chatId) {
200
+ return { action: 'drop', access, changed, reason: 'group pairing already pending' };
201
+ }
202
+ }
203
+ if (Object.keys(access.pending).length >= MAX_PENDING) {
204
+ return { action: 'drop', access, changed, reason: 'too many pending pairings' };
205
+ }
206
+ const entry = {
207
+ kind: 'group',
208
+ senderId: input.senderId,
209
+ chatId: input.chatId,
210
+ createdAt: input.now,
211
+ expiresAt: input.now + PAIRING_TTL_MS,
212
+ replies: 1,
213
+ };
214
+ const nextAccess = {
215
+ ...access,
216
+ pending: { ...access.pending, [input.newCode]: entry },
217
+ };
218
+ return { action: 'pair', access: nextAccess, changed: true, code: input.newCode, isResend: false };
219
+ }
220
+ /** True when one of `mentions` resolves to the bot's own open_id. */
221
+ export function isBotMentioned(mentions, botOpenId) {
222
+ if (!mentions || !botOpenId)
223
+ return false;
224
+ return mentions.some((m) => (m.id?.open_id ?? m.id?.union_id) === botOpenId);
225
+ }
226
+ /**
227
+ * Drop pairing entries whose `expiresAt` is at or before `now`. Returns the
228
+ * same object reference when nothing expired, so callers can cheaply skip a
229
+ * disk write via the `changed` flag.
230
+ */
231
+ export function pruneExpiredPending(access, now) {
232
+ const kept = {};
233
+ let changed = false;
234
+ for (const [code, entry] of Object.entries(access.pending)) {
235
+ if (entry.expiresAt > now) {
236
+ kept[code] = entry;
237
+ }
238
+ else {
239
+ changed = true;
240
+ }
241
+ }
242
+ return changed ? { access: { ...access, pending: kept }, changed: true } : { access, changed: false };
243
+ }
244
+ /**
245
+ * True when `senderType` identifies a Feishu bot or app.
246
+ * Feishu uses `'bot'` for cross-bot messages and `'app'` for custom-bot
247
+ * messages in some event contexts; both are non-human senders.
248
+ */
249
+ export function isBotSenderType(senderType) {
250
+ return senderType === 'bot' || senderType === 'app';
251
+ }
252
+ /**
253
+ * True when the given group is "authorized" — i.e. the channel is actively
254
+ * serving it and ambient side effects (like /introduce recording) are appropriate.
255
+ *
256
+ * - `block` → never authorized; the bot ignores all groups.
257
+ * - `follow-user` → always authorized; any group can receive messages.
258
+ * - `allowlist` → authorized only when the group has been paired and is
259
+ * present in `access.groups`. When `senderId` is provided,
260
+ * also checks that the sender passes the group's `allowFrom`
261
+ * filter (empty allowFrom = no restriction).
262
+ */
263
+ export function isGroupAuthorized(access, chatId, senderId) {
264
+ if (access.groupPolicy === 'block')
265
+ return false;
266
+ if (access.groupPolicy === 'follow-user')
267
+ return true;
268
+ const policy = access.groups[chatId];
269
+ if (!policy)
270
+ return false;
271
+ if (senderId !== undefined && policy.allowFrom.length > 0 && !policy.allowFrom.includes(senderId)) {
272
+ return false;
273
+ }
274
+ return true;
275
+ }
276
+ //# sourceMappingURL=gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../src/policy/gate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Pairing codes — the short secret a new sender must echo back through the
3
+ * access skill before the channel will trust them. Ported verbatim from
4
+ * claudemux.
5
+ */
6
+ /** A pairing code is this many random bytes, rendered as lowercase hex. */
7
+ export declare const PAIRING_CODE_BYTES = 3;
8
+ /** Resulting code length in characters (two hex digits per byte). */
9
+ export declare const PAIRING_CODE_LENGTH: number;
10
+ /** Generate a fresh, cryptographically-random pairing code. */
11
+ export declare function generatePairingCode(): string;
12
+ //# sourceMappingURL=pairing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/policy/pairing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,IAAI,CAAA;AACnC,qEAAqE;AACrE,eAAO,MAAM,mBAAmB,QAAyB,CAAA;AAEzD,+DAA+D;AAC/D,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Pairing codes — the short secret a new sender must echo back through the
3
+ * access skill before the channel will trust them. Ported verbatim from
4
+ * claudemux.
5
+ */
6
+ import { randomBytes } from 'node:crypto';
7
+ /** A pairing code is this many random bytes, rendered as lowercase hex. */
8
+ export const PAIRING_CODE_BYTES = 3;
9
+ /** Resulting code length in characters (two hex digits per byte). */
10
+ export const PAIRING_CODE_LENGTH = PAIRING_CODE_BYTES * 2;
11
+ /** Generate a fresh, cryptographically-random pairing code. */
12
+ export function generatePairingCode() {
13
+ return randomBytes(PAIRING_CODE_BYTES).toString('hex');
14
+ }
15
+ //# sourceMappingURL=pairing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.js","sourceRoot":"","sources":["../../src/policy/pairing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAA;AACnC,qEAAqE;AACrE,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,GAAG,CAAC,CAAA;AAEzD,+DAA+D;AAC/D,MAAM,UAAU,mBAAmB;IACjC,OAAO,WAAW,CAAC,kBAAkB,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AACxD,CAAC"}