@rozek/nanoclaw 0.0.4 → 0.0.6
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.
- package/container/agent-runner/package-lock.json +1524 -0
- package/dist/cli.js +75 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -1
- package/package.json +7 -1
- package/.claude/settings.json +0 -1
- package/.claude/skills/add-compact/SKILL.md +0 -135
- package/.claude/skills/add-discord/SKILL.md +0 -203
- package/.claude/skills/add-gmail/SKILL.md +0 -220
- package/.claude/skills/add-image-vision/SKILL.md +0 -94
- package/.claude/skills/add-ollama-tool/SKILL.md +0 -153
- package/.claude/skills/add-parallel/SKILL.md +0 -290
- package/.claude/skills/add-pdf-reader/SKILL.md +0 -104
- package/.claude/skills/add-reactions/SKILL.md +0 -117
- package/.claude/skills/add-slack/SKILL.md +0 -207
- package/.claude/skills/add-telegram/SKILL.md +0 -222
- package/.claude/skills/add-telegram-swarm/SKILL.md +0 -384
- package/.claude/skills/add-voice-transcription/SKILL.md +0 -148
- package/.claude/skills/add-whatsapp/SKILL.md +0 -372
- package/.claude/skills/convert-to-apple-container/SKILL.md +0 -175
- package/.claude/skills/customize/SKILL.md +0 -110
- package/.claude/skills/debug/SKILL.md +0 -349
- package/.claude/skills/get-qodo-rules/SKILL.md +0 -122
- package/.claude/skills/get-qodo-rules/references/output-format.md +0 -41
- package/.claude/skills/get-qodo-rules/references/pagination.md +0 -33
- package/.claude/skills/get-qodo-rules/references/repository-scope.md +0 -26
- package/.claude/skills/qodo-pr-resolver/SKILL.md +0 -326
- package/.claude/skills/qodo-pr-resolver/resources/providers.md +0 -329
- package/.claude/skills/setup/SKILL.md +0 -218
- package/.claude/skills/update-nanoclaw/SKILL.md +0 -235
- package/.claude/skills/update-skills/SKILL.md +0 -130
- package/.claude/skills/use-local-whisper/SKILL.md +0 -152
- package/.claude/skills/x-integration/SKILL.md +0 -417
- package/.claude/skills/x-integration/agent.ts +0 -243
- package/.claude/skills/x-integration/host.ts +0 -159
- package/.claude/skills/x-integration/lib/browser.ts +0 -148
- package/.claude/skills/x-integration/lib/config.ts +0 -62
- package/.claude/skills/x-integration/scripts/like.ts +0 -56
- package/.claude/skills/x-integration/scripts/post.ts +0 -66
- package/.claude/skills/x-integration/scripts/quote.ts +0 -80
- package/.claude/skills/x-integration/scripts/reply.ts +0 -74
- package/.claude/skills/x-integration/scripts/retweet.ts +0 -62
- package/.claude/skills/x-integration/scripts/setup.ts +0 -87
- package/.env.example +0 -1
- package/.github/CODEOWNERS +0 -10
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -14
- package/.github/workflows/bump-version.yml +0 -32
- package/.github/workflows/ci.yml +0 -25
- package/.github/workflows/merge-forward-skills.yml +0 -160
- package/.github/workflows/update-tokens.yml +0 -42
- package/.husky/pre-commit +0 -1
- package/.mcp.json +0 -3
- package/.nvmrc +0 -1
- package/.prettierrc +0 -3
- package/CHANGELOG.md +0 -8
- package/CONTRIBUTING.md +0 -23
- package/CONTRIBUTORS.md +0 -15
- package/NanoClaw_with_Web-Support.md +0 -325
- package/README_zh.md +0 -200
- package/assets/nanoclaw-favicon.png +0 -0
- package/assets/nanoclaw-icon.png +0 -0
- package/assets/nanoclaw-logo-dark.png +0 -0
- package/assets/nanoclaw-logo.png +0 -0
- package/assets/nanoclaw-profile.jpeg +0 -0
- package/assets/nanoclaw-sales.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/config-examples/mount-allowlist.json +0 -25
- package/docs/APPLE-CONTAINER-NETWORKING.md +0 -90
- package/docs/DEBUG_CHECKLIST.md +0 -143
- package/docs/REQUIREMENTS.md +0 -196
- package/docs/SDK_DEEP_DIVE.md +0 -643
- package/docs/SECURITY.md +0 -122
- package/docs/SPEC.md +0 -785
- package/docs/docker-sandboxes.md +0 -359
- package/docs/nanoclaw-architecture-final.md +0 -1063
- package/docs/nanorepo-architecture.md +0 -168
- package/docs/skills-as-branches.md +0 -662
- package/groups/global/CLAUDE.md +0 -58
- package/groups/main/CLAUDE.md +0 -246
- package/launchd/com.nanoclaw.plist +0 -32
- package/repo-tokens/README.md +0 -113
- package/repo-tokens/action.yml +0 -186
- package/repo-tokens/badge.svg +0 -23
- package/repo-tokens/examples/green.svg +0 -14
- package/repo-tokens/examples/red.svg +0 -14
- package/repo-tokens/examples/yellow-green.svg +0 -14
- package/repo-tokens/examples/yellow.svg +0 -14
- package/scripts/run-migrations.ts +0 -105
- package/setup.sh +0 -161
- package/src/channels/index.ts +0 -15
- package/src/channels/registry.test.ts +0 -42
- package/src/channels/registry.ts +0 -32
- package/src/channels/web.ts +0 -1931
- package/src/cli.ts +0 -210
- package/src/config.ts +0 -73
- package/src/container-runner.test.ts +0 -210
- package/src/container-runner.ts +0 -768
- package/src/container-runtime.test.ts +0 -149
- package/src/container-runtime.ts +0 -127
- package/src/credential-proxy.test.ts +0 -192
- package/src/credential-proxy.ts +0 -125
- package/src/db.test.ts +0 -484
- package/src/db.ts +0 -803
- package/src/env.ts +0 -42
- package/src/formatting.test.ts +0 -256
- package/src/group-folder.test.ts +0 -43
- package/src/group-folder.ts +0 -44
- package/src/group-queue.test.ts +0 -484
- package/src/group-queue.ts +0 -379
- package/src/index.ts +0 -854
- package/src/ipc-auth.test.ts +0 -679
- package/src/ipc.ts +0 -461
- package/src/logger.ts +0 -16
- package/src/mount-security.ts +0 -419
- package/src/remote-control.test.ts +0 -397
- package/src/remote-control.ts +0 -224
- package/src/router.ts +0 -52
- package/src/routing.test.ts +0 -170
- package/src/sender-allowlist.test.ts +0 -216
- package/src/sender-allowlist.ts +0 -128
- package/src/session-commands.test.ts +0 -247
- package/src/session-commands.ts +0 -163
- package/src/task-scheduler.test.ts +0 -129
- package/src/task-scheduler.ts +0 -328
- package/src/timezone.test.ts +0 -29
- package/src/timezone.ts +0 -16
- package/src/types.ts +0 -109
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -7
- package/vitest.skills.config.ts +0 -7
package/src/routing.test.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { _initTestDatabase, getAllChats, storeChatMetadata } from './db.js';
|
|
4
|
-
import { getAvailableGroups, _setRegisteredGroups } from './index.js';
|
|
5
|
-
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
_initTestDatabase();
|
|
8
|
-
_setRegisteredGroups({});
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
// --- JID ownership patterns ---
|
|
12
|
-
|
|
13
|
-
describe('JID ownership patterns', () => {
|
|
14
|
-
// These test the patterns that will become ownsJid() on the Channel interface
|
|
15
|
-
|
|
16
|
-
it('WhatsApp group JID: ends with @g.us', () => {
|
|
17
|
-
const jid = '12345678@g.us';
|
|
18
|
-
expect(jid.endsWith('@g.us')).toBe(true);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('WhatsApp DM JID: ends with @s.whatsapp.net', () => {
|
|
22
|
-
const jid = '12345678@s.whatsapp.net';
|
|
23
|
-
expect(jid.endsWith('@s.whatsapp.net')).toBe(true);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// --- getAvailableGroups ---
|
|
28
|
-
|
|
29
|
-
describe('getAvailableGroups', () => {
|
|
30
|
-
it('returns only groups, excludes DMs', () => {
|
|
31
|
-
storeChatMetadata(
|
|
32
|
-
'group1@g.us',
|
|
33
|
-
'2024-01-01T00:00:01.000Z',
|
|
34
|
-
'Group 1',
|
|
35
|
-
'whatsapp',
|
|
36
|
-
true,
|
|
37
|
-
);
|
|
38
|
-
storeChatMetadata(
|
|
39
|
-
'user@s.whatsapp.net',
|
|
40
|
-
'2024-01-01T00:00:02.000Z',
|
|
41
|
-
'User DM',
|
|
42
|
-
'whatsapp',
|
|
43
|
-
false,
|
|
44
|
-
);
|
|
45
|
-
storeChatMetadata(
|
|
46
|
-
'group2@g.us',
|
|
47
|
-
'2024-01-01T00:00:03.000Z',
|
|
48
|
-
'Group 2',
|
|
49
|
-
'whatsapp',
|
|
50
|
-
true,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const groups = getAvailableGroups();
|
|
54
|
-
expect(groups).toHaveLength(2);
|
|
55
|
-
expect(groups.map((g) => g.jid)).toContain('group1@g.us');
|
|
56
|
-
expect(groups.map((g) => g.jid)).toContain('group2@g.us');
|
|
57
|
-
expect(groups.map((g) => g.jid)).not.toContain('user@s.whatsapp.net');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('excludes __group_sync__ sentinel', () => {
|
|
61
|
-
storeChatMetadata('__group_sync__', '2024-01-01T00:00:00.000Z');
|
|
62
|
-
storeChatMetadata(
|
|
63
|
-
'group@g.us',
|
|
64
|
-
'2024-01-01T00:00:01.000Z',
|
|
65
|
-
'Group',
|
|
66
|
-
'whatsapp',
|
|
67
|
-
true,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const groups = getAvailableGroups();
|
|
71
|
-
expect(groups).toHaveLength(1);
|
|
72
|
-
expect(groups[0].jid).toBe('group@g.us');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('marks registered groups correctly', () => {
|
|
76
|
-
storeChatMetadata(
|
|
77
|
-
'reg@g.us',
|
|
78
|
-
'2024-01-01T00:00:01.000Z',
|
|
79
|
-
'Registered',
|
|
80
|
-
'whatsapp',
|
|
81
|
-
true,
|
|
82
|
-
);
|
|
83
|
-
storeChatMetadata(
|
|
84
|
-
'unreg@g.us',
|
|
85
|
-
'2024-01-01T00:00:02.000Z',
|
|
86
|
-
'Unregistered',
|
|
87
|
-
'whatsapp',
|
|
88
|
-
true,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
_setRegisteredGroups({
|
|
92
|
-
'reg@g.us': {
|
|
93
|
-
name: 'Registered',
|
|
94
|
-
folder: 'registered',
|
|
95
|
-
trigger: '@Andy',
|
|
96
|
-
added_at: '2024-01-01T00:00:00.000Z',
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const groups = getAvailableGroups();
|
|
101
|
-
const reg = groups.find((g) => g.jid === 'reg@g.us');
|
|
102
|
-
const unreg = groups.find((g) => g.jid === 'unreg@g.us');
|
|
103
|
-
|
|
104
|
-
expect(reg?.isRegistered).toBe(true);
|
|
105
|
-
expect(unreg?.isRegistered).toBe(false);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('returns groups ordered by most recent activity', () => {
|
|
109
|
-
storeChatMetadata(
|
|
110
|
-
'old@g.us',
|
|
111
|
-
'2024-01-01T00:00:01.000Z',
|
|
112
|
-
'Old',
|
|
113
|
-
'whatsapp',
|
|
114
|
-
true,
|
|
115
|
-
);
|
|
116
|
-
storeChatMetadata(
|
|
117
|
-
'new@g.us',
|
|
118
|
-
'2024-01-01T00:00:05.000Z',
|
|
119
|
-
'New',
|
|
120
|
-
'whatsapp',
|
|
121
|
-
true,
|
|
122
|
-
);
|
|
123
|
-
storeChatMetadata(
|
|
124
|
-
'mid@g.us',
|
|
125
|
-
'2024-01-01T00:00:03.000Z',
|
|
126
|
-
'Mid',
|
|
127
|
-
'whatsapp',
|
|
128
|
-
true,
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
const groups = getAvailableGroups();
|
|
132
|
-
expect(groups[0].jid).toBe('new@g.us');
|
|
133
|
-
expect(groups[1].jid).toBe('mid@g.us');
|
|
134
|
-
expect(groups[2].jid).toBe('old@g.us');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('excludes non-group chats regardless of JID format', () => {
|
|
138
|
-
// Unknown JID format stored without is_group should not appear
|
|
139
|
-
storeChatMetadata(
|
|
140
|
-
'unknown-format-123',
|
|
141
|
-
'2024-01-01T00:00:01.000Z',
|
|
142
|
-
'Unknown',
|
|
143
|
-
);
|
|
144
|
-
// Explicitly non-group with unusual JID
|
|
145
|
-
storeChatMetadata(
|
|
146
|
-
'custom:abc',
|
|
147
|
-
'2024-01-01T00:00:02.000Z',
|
|
148
|
-
'Custom DM',
|
|
149
|
-
'custom',
|
|
150
|
-
false,
|
|
151
|
-
);
|
|
152
|
-
// A real group for contrast
|
|
153
|
-
storeChatMetadata(
|
|
154
|
-
'group@g.us',
|
|
155
|
-
'2024-01-01T00:00:03.000Z',
|
|
156
|
-
'Group',
|
|
157
|
-
'whatsapp',
|
|
158
|
-
true,
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const groups = getAvailableGroups();
|
|
162
|
-
expect(groups).toHaveLength(1);
|
|
163
|
-
expect(groups[0].jid).toBe('group@g.us');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('returns empty array when no chats exist', () => {
|
|
167
|
-
const groups = getAvailableGroups();
|
|
168
|
-
expect(groups).toHaveLength(0);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
isSenderAllowed,
|
|
8
|
-
isTriggerAllowed,
|
|
9
|
-
loadSenderAllowlist,
|
|
10
|
-
SenderAllowlistConfig,
|
|
11
|
-
shouldDropMessage,
|
|
12
|
-
} from './sender-allowlist.js';
|
|
13
|
-
|
|
14
|
-
let tmpDir: string;
|
|
15
|
-
|
|
16
|
-
function cfgPath(name = 'sender-allowlist.json'): string {
|
|
17
|
-
return path.join(tmpDir, name);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function writeConfig(config: unknown, name?: string): string {
|
|
21
|
-
const p = cfgPath(name);
|
|
22
|
-
fs.writeFileSync(p, JSON.stringify(config));
|
|
23
|
-
return p;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'allowlist-test-'));
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
afterEach(() => {
|
|
31
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('loadSenderAllowlist', () => {
|
|
35
|
-
it('returns allow-all defaults when file is missing', () => {
|
|
36
|
-
const cfg = loadSenderAllowlist(cfgPath());
|
|
37
|
-
expect(cfg.default.allow).toBe('*');
|
|
38
|
-
expect(cfg.default.mode).toBe('trigger');
|
|
39
|
-
expect(cfg.logDenied).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('loads allow=* config', () => {
|
|
43
|
-
const p = writeConfig({
|
|
44
|
-
default: { allow: '*', mode: 'trigger' },
|
|
45
|
-
chats: {},
|
|
46
|
-
logDenied: false,
|
|
47
|
-
});
|
|
48
|
-
const cfg = loadSenderAllowlist(p);
|
|
49
|
-
expect(cfg.default.allow).toBe('*');
|
|
50
|
-
expect(cfg.logDenied).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('loads allow=[] (deny all)', () => {
|
|
54
|
-
const p = writeConfig({
|
|
55
|
-
default: { allow: [], mode: 'trigger' },
|
|
56
|
-
chats: {},
|
|
57
|
-
});
|
|
58
|
-
const cfg = loadSenderAllowlist(p);
|
|
59
|
-
expect(cfg.default.allow).toEqual([]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('loads allow=[list]', () => {
|
|
63
|
-
const p = writeConfig({
|
|
64
|
-
default: { allow: ['alice', 'bob'], mode: 'drop' },
|
|
65
|
-
chats: {},
|
|
66
|
-
});
|
|
67
|
-
const cfg = loadSenderAllowlist(p);
|
|
68
|
-
expect(cfg.default.allow).toEqual(['alice', 'bob']);
|
|
69
|
-
expect(cfg.default.mode).toBe('drop');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('per-chat override beats default', () => {
|
|
73
|
-
const p = writeConfig({
|
|
74
|
-
default: { allow: '*', mode: 'trigger' },
|
|
75
|
-
chats: { 'group-a': { allow: ['alice'], mode: 'drop' } },
|
|
76
|
-
});
|
|
77
|
-
const cfg = loadSenderAllowlist(p);
|
|
78
|
-
expect(cfg.chats['group-a'].allow).toEqual(['alice']);
|
|
79
|
-
expect(cfg.chats['group-a'].mode).toBe('drop');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('returns allow-all on invalid JSON', () => {
|
|
83
|
-
const p = cfgPath();
|
|
84
|
-
fs.writeFileSync(p, '{ not valid json }}}');
|
|
85
|
-
const cfg = loadSenderAllowlist(p);
|
|
86
|
-
expect(cfg.default.allow).toBe('*');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('returns allow-all on invalid schema', () => {
|
|
90
|
-
const p = writeConfig({ default: { oops: true } });
|
|
91
|
-
const cfg = loadSenderAllowlist(p);
|
|
92
|
-
expect(cfg.default.allow).toBe('*');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('rejects non-string allow array items', () => {
|
|
96
|
-
const p = writeConfig({
|
|
97
|
-
default: { allow: [123, null, true], mode: 'trigger' },
|
|
98
|
-
chats: {},
|
|
99
|
-
});
|
|
100
|
-
const cfg = loadSenderAllowlist(p);
|
|
101
|
-
expect(cfg.default.allow).toBe('*'); // falls back to default
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('skips invalid per-chat entries', () => {
|
|
105
|
-
const p = writeConfig({
|
|
106
|
-
default: { allow: '*', mode: 'trigger' },
|
|
107
|
-
chats: {
|
|
108
|
-
good: { allow: ['alice'], mode: 'trigger' },
|
|
109
|
-
bad: { allow: 123 },
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
const cfg = loadSenderAllowlist(p);
|
|
113
|
-
expect(cfg.chats['good']).toBeDefined();
|
|
114
|
-
expect(cfg.chats['bad']).toBeUndefined();
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('isSenderAllowed', () => {
|
|
119
|
-
it('allow=* allows any sender', () => {
|
|
120
|
-
const cfg: SenderAllowlistConfig = {
|
|
121
|
-
default: { allow: '*', mode: 'trigger' },
|
|
122
|
-
chats: {},
|
|
123
|
-
logDenied: true,
|
|
124
|
-
};
|
|
125
|
-
expect(isSenderAllowed('g1', 'anyone', cfg)).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('allow=[] denies any sender', () => {
|
|
129
|
-
const cfg: SenderAllowlistConfig = {
|
|
130
|
-
default: { allow: [], mode: 'trigger' },
|
|
131
|
-
chats: {},
|
|
132
|
-
logDenied: true,
|
|
133
|
-
};
|
|
134
|
-
expect(isSenderAllowed('g1', 'anyone', cfg)).toBe(false);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('allow=[list] allows exact match only', () => {
|
|
138
|
-
const cfg: SenderAllowlistConfig = {
|
|
139
|
-
default: { allow: ['alice', 'bob'], mode: 'trigger' },
|
|
140
|
-
chats: {},
|
|
141
|
-
logDenied: true,
|
|
142
|
-
};
|
|
143
|
-
expect(isSenderAllowed('g1', 'alice', cfg)).toBe(true);
|
|
144
|
-
expect(isSenderAllowed('g1', 'eve', cfg)).toBe(false);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('uses per-chat entry over default', () => {
|
|
148
|
-
const cfg: SenderAllowlistConfig = {
|
|
149
|
-
default: { allow: '*', mode: 'trigger' },
|
|
150
|
-
chats: { g1: { allow: ['alice'], mode: 'trigger' } },
|
|
151
|
-
logDenied: true,
|
|
152
|
-
};
|
|
153
|
-
expect(isSenderAllowed('g1', 'bob', cfg)).toBe(false);
|
|
154
|
-
expect(isSenderAllowed('g2', 'bob', cfg)).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe('shouldDropMessage', () => {
|
|
159
|
-
it('returns false for trigger mode', () => {
|
|
160
|
-
const cfg: SenderAllowlistConfig = {
|
|
161
|
-
default: { allow: '*', mode: 'trigger' },
|
|
162
|
-
chats: {},
|
|
163
|
-
logDenied: true,
|
|
164
|
-
};
|
|
165
|
-
expect(shouldDropMessage('g1', cfg)).toBe(false);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('returns true for drop mode', () => {
|
|
169
|
-
const cfg: SenderAllowlistConfig = {
|
|
170
|
-
default: { allow: '*', mode: 'drop' },
|
|
171
|
-
chats: {},
|
|
172
|
-
logDenied: true,
|
|
173
|
-
};
|
|
174
|
-
expect(shouldDropMessage('g1', cfg)).toBe(true);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('per-chat mode override', () => {
|
|
178
|
-
const cfg: SenderAllowlistConfig = {
|
|
179
|
-
default: { allow: '*', mode: 'trigger' },
|
|
180
|
-
chats: { g1: { allow: '*', mode: 'drop' } },
|
|
181
|
-
logDenied: true,
|
|
182
|
-
};
|
|
183
|
-
expect(shouldDropMessage('g1', cfg)).toBe(true);
|
|
184
|
-
expect(shouldDropMessage('g2', cfg)).toBe(false);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
describe('isTriggerAllowed', () => {
|
|
189
|
-
it('allows trigger for allowed sender', () => {
|
|
190
|
-
const cfg: SenderAllowlistConfig = {
|
|
191
|
-
default: { allow: ['alice'], mode: 'trigger' },
|
|
192
|
-
chats: {},
|
|
193
|
-
logDenied: false,
|
|
194
|
-
};
|
|
195
|
-
expect(isTriggerAllowed('g1', 'alice', cfg)).toBe(true);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('denies trigger for disallowed sender', () => {
|
|
199
|
-
const cfg: SenderAllowlistConfig = {
|
|
200
|
-
default: { allow: ['alice'], mode: 'trigger' },
|
|
201
|
-
chats: {},
|
|
202
|
-
logDenied: false,
|
|
203
|
-
};
|
|
204
|
-
expect(isTriggerAllowed('g1', 'eve', cfg)).toBe(false);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('logs when logDenied is true', () => {
|
|
208
|
-
const cfg: SenderAllowlistConfig = {
|
|
209
|
-
default: { allow: ['alice'], mode: 'trigger' },
|
|
210
|
-
chats: {},
|
|
211
|
-
logDenied: true,
|
|
212
|
-
};
|
|
213
|
-
isTriggerAllowed('g1', 'eve', cfg);
|
|
214
|
-
// Logger.debug is called — we just verify no crash; logger is a real pino instance
|
|
215
|
-
});
|
|
216
|
-
});
|
package/src/sender-allowlist.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
|
|
3
|
-
import { SENDER_ALLOWLIST_PATH } from './config.js';
|
|
4
|
-
import { logger } from './logger.js';
|
|
5
|
-
|
|
6
|
-
export interface ChatAllowlistEntry {
|
|
7
|
-
allow: '*' | string[];
|
|
8
|
-
mode: 'trigger' | 'drop';
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface SenderAllowlistConfig {
|
|
12
|
-
default: ChatAllowlistEntry;
|
|
13
|
-
chats: Record<string, ChatAllowlistEntry>;
|
|
14
|
-
logDenied: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const DEFAULT_CONFIG: SenderAllowlistConfig = {
|
|
18
|
-
default: { allow: '*', mode: 'trigger' },
|
|
19
|
-
chats: {},
|
|
20
|
-
logDenied: true,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function isValidEntry(entry: unknown): entry is ChatAllowlistEntry {
|
|
24
|
-
if (!entry || typeof entry !== 'object') return false;
|
|
25
|
-
const e = entry as Record<string, unknown>;
|
|
26
|
-
const validAllow =
|
|
27
|
-
e.allow === '*' ||
|
|
28
|
-
(Array.isArray(e.allow) && e.allow.every((v) => typeof v === 'string'));
|
|
29
|
-
const validMode = e.mode === 'trigger' || e.mode === 'drop';
|
|
30
|
-
return validAllow && validMode;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function loadSenderAllowlist(
|
|
34
|
-
pathOverride?: string,
|
|
35
|
-
): SenderAllowlistConfig {
|
|
36
|
-
const filePath = pathOverride ?? SENDER_ALLOWLIST_PATH;
|
|
37
|
-
|
|
38
|
-
let raw: string;
|
|
39
|
-
try {
|
|
40
|
-
raw = fs.readFileSync(filePath, 'utf-8');
|
|
41
|
-
} catch (err: unknown) {
|
|
42
|
-
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return DEFAULT_CONFIG;
|
|
43
|
-
logger.warn(
|
|
44
|
-
{ err, path: filePath },
|
|
45
|
-
'sender-allowlist: cannot read config',
|
|
46
|
-
);
|
|
47
|
-
return DEFAULT_CONFIG;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
let parsed: unknown;
|
|
51
|
-
try {
|
|
52
|
-
parsed = JSON.parse(raw);
|
|
53
|
-
} catch {
|
|
54
|
-
logger.warn({ path: filePath }, 'sender-allowlist: invalid JSON');
|
|
55
|
-
return DEFAULT_CONFIG;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const obj = parsed as Record<string, unknown>;
|
|
59
|
-
|
|
60
|
-
if (!isValidEntry(obj.default)) {
|
|
61
|
-
logger.warn(
|
|
62
|
-
{ path: filePath },
|
|
63
|
-
'sender-allowlist: invalid or missing default entry',
|
|
64
|
-
);
|
|
65
|
-
return DEFAULT_CONFIG;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const chats: Record<string, ChatAllowlistEntry> = {};
|
|
69
|
-
if (obj.chats && typeof obj.chats === 'object') {
|
|
70
|
-
for (const [jid, entry] of Object.entries(
|
|
71
|
-
obj.chats as Record<string, unknown>,
|
|
72
|
-
)) {
|
|
73
|
-
if (isValidEntry(entry)) {
|
|
74
|
-
chats[jid] = entry;
|
|
75
|
-
} else {
|
|
76
|
-
logger.warn(
|
|
77
|
-
{ jid, path: filePath },
|
|
78
|
-
'sender-allowlist: skipping invalid chat entry',
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
default: obj.default as ChatAllowlistEntry,
|
|
86
|
-
chats,
|
|
87
|
-
logDenied: obj.logDenied !== false,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function getEntry(
|
|
92
|
-
chatJid: string,
|
|
93
|
-
cfg: SenderAllowlistConfig,
|
|
94
|
-
): ChatAllowlistEntry {
|
|
95
|
-
return cfg.chats[chatJid] ?? cfg.default;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function isSenderAllowed(
|
|
99
|
-
chatJid: string,
|
|
100
|
-
sender: string,
|
|
101
|
-
cfg: SenderAllowlistConfig,
|
|
102
|
-
): boolean {
|
|
103
|
-
const entry = getEntry(chatJid, cfg);
|
|
104
|
-
if (entry.allow === '*') return true;
|
|
105
|
-
return entry.allow.includes(sender);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function shouldDropMessage(
|
|
109
|
-
chatJid: string,
|
|
110
|
-
cfg: SenderAllowlistConfig,
|
|
111
|
-
): boolean {
|
|
112
|
-
return getEntry(chatJid, cfg).mode === 'drop';
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function isTriggerAllowed(
|
|
116
|
-
chatJid: string,
|
|
117
|
-
sender: string,
|
|
118
|
-
cfg: SenderAllowlistConfig,
|
|
119
|
-
): boolean {
|
|
120
|
-
const allowed = isSenderAllowed(chatJid, sender, cfg);
|
|
121
|
-
if (!allowed && cfg.logDenied) {
|
|
122
|
-
logger.debug(
|
|
123
|
-
{ chatJid, sender },
|
|
124
|
-
'sender-allowlist: trigger denied for sender',
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
return allowed;
|
|
128
|
-
}
|