@nopeek/agent-bridge 0.1.0 → 0.1.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.
- package/dist/bot.d.ts +10 -0
- package/dist/bot.js +50 -3
- package/dist/bridge.js +6 -3
- package/dist/config.d.ts +3 -0
- package/dist/config.js +1 -0
- package/package.json +1 -1
package/dist/bot.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ export declare class BotRunner {
|
|
|
18
18
|
private refreshTimer;
|
|
19
19
|
private seen;
|
|
20
20
|
private channelCache;
|
|
21
|
+
private allowed;
|
|
22
|
+
private accessLoaded;
|
|
23
|
+
private declined;
|
|
21
24
|
private log;
|
|
22
25
|
private logErr;
|
|
23
26
|
constructor(info: BotInfo, cfg: BridgeConfig);
|
|
@@ -25,6 +28,13 @@ export declare class BotRunner {
|
|
|
25
28
|
start(): void;
|
|
26
29
|
stop(): void;
|
|
27
30
|
private mintSession;
|
|
31
|
+
/** Fetch (or refresh) the set of users allowed to talk to this bot. Called on
|
|
32
|
+
* connect and whenever a grant changes. Best-effort: on failure we keep the
|
|
33
|
+
* last-known set, and if we've never loaded one we FAIL CLOSED (owner-only)
|
|
34
|
+
* rather than open. */
|
|
35
|
+
refreshAccess(): Promise<void>;
|
|
36
|
+
/** Sender permitted to talk to this bot? Fail closed if access never loaded. */
|
|
37
|
+
private isAllowed;
|
|
28
38
|
private run;
|
|
29
39
|
private connectOnce;
|
|
30
40
|
/** Runtime sessions expire; reconnect with a fresh one shortly before that. */
|
package/dist/bot.js
CHANGED
|
@@ -22,6 +22,13 @@ export class BotRunner {
|
|
|
22
22
|
refreshTimer = null;
|
|
23
23
|
seen = new Set();
|
|
24
24
|
channelCache = new Map();
|
|
25
|
+
// Access control: bots are PRIVATE by default. `allowed` is the set of user
|
|
26
|
+
// ids permitted to talk to this bot (owner + workspace members + granted
|
|
27
|
+
// users), fetched from the server. A sender NOT in this set never reaches the
|
|
28
|
+
// brain — so a stranger who finds the @handle can't drive the owner's agent.
|
|
29
|
+
allowed = new Set();
|
|
30
|
+
accessLoaded = false;
|
|
31
|
+
declined = new Set(); // (senderId) already told "not authorized" once
|
|
25
32
|
log;
|
|
26
33
|
logErr;
|
|
27
34
|
constructor(info, cfg) {
|
|
@@ -67,6 +74,32 @@ export class BotRunner {
|
|
|
67
74
|
}
|
|
68
75
|
return (await res.json());
|
|
69
76
|
}
|
|
77
|
+
/** Fetch (or refresh) the set of users allowed to talk to this bot. Called on
|
|
78
|
+
* connect and whenever a grant changes. Best-effort: on failure we keep the
|
|
79
|
+
* last-known set, and if we've never loaded one we FAIL CLOSED (owner-only)
|
|
80
|
+
* rather than open. */
|
|
81
|
+
async refreshAccess() {
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`${this.cfg.apiUrl}/v1/apps/${this.cfg.appId}/bots/${this.info.userId}/access`, { headers: { authorization: `Bearer ${this.cfg.pairingCode}` } });
|
|
84
|
+
if (!res.ok)
|
|
85
|
+
throw new Error(`access HTTP ${res.status}`);
|
|
86
|
+
const j = (await res.json());
|
|
87
|
+
this.allowed = new Set(j.allowedUserIds ?? []);
|
|
88
|
+
this.accessLoaded = true;
|
|
89
|
+
this.log(`access: policy=${j.policy ?? "private"}, ${this.allowed.size} allowed sender(s)`);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
this.logErr(`access refresh failed: ${err.message} (keeping last-known allow list)`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Sender permitted to talk to this bot? Fail closed if access never loaded. */
|
|
96
|
+
isAllowed(senderUserId) {
|
|
97
|
+
if (senderUserId === this.info.ownerId)
|
|
98
|
+
return true; // owner always
|
|
99
|
+
if (!this.accessLoaded)
|
|
100
|
+
return false; // fail closed until we know the list
|
|
101
|
+
return this.allowed.has(senderUserId);
|
|
102
|
+
}
|
|
70
103
|
async run() {
|
|
71
104
|
this.log(`starting (${this.info.userId}) brain=${this.brainKind}`);
|
|
72
105
|
let delay = 2_000;
|
|
@@ -121,6 +154,7 @@ export class BotRunner {
|
|
|
121
154
|
this.logErr(`handler error for ${m.messageId}: ${err.message}`);
|
|
122
155
|
});
|
|
123
156
|
}));
|
|
157
|
+
await this.refreshAccess(); // load the allow list before we answer anyone
|
|
124
158
|
this.scheduleRefresh(session.expiresAt);
|
|
125
159
|
}
|
|
126
160
|
/** Runtime sessions expire; reconnect with a fresh one shortly before that. */
|
|
@@ -193,9 +227,22 @@ export class BotRunner {
|
|
|
193
227
|
}
|
|
194
228
|
if (m.body?.type !== "text" || typeof m.body.text !== "string" || !m.body.text.trim())
|
|
195
229
|
return;
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
230
|
+
// ACCESS CONTROL: a bot is private. If the sender isn't authorized (owner,
|
|
231
|
+
// workspace member, or explicitly granted), the brain is NEVER invoked — so
|
|
232
|
+
// a stranger who found the @handle can't make the owner's agent do anything.
|
|
233
|
+
if (!this.isAllowed(m.senderUserId)) {
|
|
234
|
+
this.log(`${m.channelId} <- ${m.senderUserId}: BLOCKED (not authorized) — brain not invoked`);
|
|
235
|
+
if (this.cfg.declineMessage && !this.declined.has(m.senderUserId)) {
|
|
236
|
+
this.declined.add(m.senderUserId);
|
|
237
|
+
try {
|
|
238
|
+
await (await this.getChannel(m.channelId)).send({ text: this.cfg.declineMessage });
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
/* best-effort decline notice */
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
199
246
|
const text = m.body.text;
|
|
200
247
|
this.log(`${m.channelId} <- ${m.senderUserId}: ${text.slice(0, 120)}`);
|
|
201
248
|
const ch = await this.getChannel(m.channelId);
|
package/dist/bridge.js
CHANGED
|
@@ -41,9 +41,12 @@ export async function runBridge(cfg) {
|
|
|
41
41
|
onAdoptBot: (f) => {
|
|
42
42
|
startBot({ userId: f.botUserId, handle: f.handle, ownerId: f.ownerUserId });
|
|
43
43
|
},
|
|
44
|
-
onGrantChanged: () => {
|
|
45
|
-
//
|
|
46
|
-
//
|
|
44
|
+
onGrantChanged: (f) => {
|
|
45
|
+
// Access changed for a bot — re-pull its allow list so enforcement is live
|
|
46
|
+
// (a revoked user stops being answered within seconds, no restart).
|
|
47
|
+
const runner = bots.get(f.botUserId);
|
|
48
|
+
if (runner)
|
|
49
|
+
void runner.refreshAccess();
|
|
47
50
|
},
|
|
48
51
|
});
|
|
49
52
|
// ---------------------------------------------------------------- health --
|
package/dist/config.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export interface BridgeConfig {
|
|
|
20
20
|
port: number;
|
|
21
21
|
/** Where per-bot device identity/key stores live (./data by default). */
|
|
22
22
|
dataDir: string;
|
|
23
|
+
/** Optional reply sent ONCE to an unauthorized sender. Null (default) =
|
|
24
|
+
* silently ignore them (most private — doesn't reveal the bot exists). */
|
|
25
|
+
declineMessage: string | null;
|
|
23
26
|
}
|
|
24
27
|
export declare const DEFAULT_API_URL = "https://d3qweh72vesa98.cloudfront.net";
|
|
25
28
|
export declare const DEFAULT_PORT = 8790;
|
package/dist/config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nopeek/agent-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Run your own agents as E2EE NoPeek bots. Pairs with a one-time code, runs every bot you own, and pipes messages to any command or webhook.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|