@inetafrica/open-claudia 2.1.1 → 2.2.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/core/actions.js +120 -2
- package/core/handlers.js +75 -19
- package/core/router.js +25 -5
- package/package.json +1 -1
- package/vault.js +3 -1
package/core/actions.js
CHANGED
|
@@ -10,6 +10,9 @@ const { currentState, resetSettings, resetSessionUsage, saveState } = require(".
|
|
|
10
10
|
const { runClaude, getActiveSessionId } = require("./runner");
|
|
11
11
|
const { listProjects, projectKeyboard, workspacePath } = require("./projects");
|
|
12
12
|
const { isChatOwner, approveAuthRequest, denyAuthRequest, authRequestLabel } = require("./access");
|
|
13
|
+
const accessStore = require("./access");
|
|
14
|
+
const peopleStore = require("./people");
|
|
15
|
+
const { vault } = require("./vault-store");
|
|
13
16
|
const intros = require("./intros");
|
|
14
17
|
const introFlow = require("./intro-flow");
|
|
15
18
|
const { finishOnboarding } = require("./onboarding");
|
|
@@ -27,8 +30,53 @@ async function handleAction(envelope) {
|
|
|
27
30
|
const state = currentState();
|
|
28
31
|
|
|
29
32
|
if (d.startsWith("auth:")) {
|
|
30
|
-
if (!isChatOwner(envelope.channelId)) return send("Owner only — auth
|
|
31
|
-
const
|
|
33
|
+
if (!isChatOwner(envelope.channelId)) return send("Owner only — auth controls are restricted.");
|
|
34
|
+
const parts = d.split(":");
|
|
35
|
+
const action = parts[1];
|
|
36
|
+
|
|
37
|
+
if (action === "list") {
|
|
38
|
+
const raw = accessStore.listAuthorizedRaw();
|
|
39
|
+
const lines = ["Authorized chats:"];
|
|
40
|
+
for (const a of raw.authorized) {
|
|
41
|
+
const person = peopleStore.findByHandle("telegram", a.chatId) || peopleStore.findByHandle("kazee", a.chatId);
|
|
42
|
+
lines.push(` ${a.chatId}${a.isOwner ? " (owner)" : ""} ${a.name || a.username || ""}${person ? " → " + person.name : " → (no person)"}`);
|
|
43
|
+
}
|
|
44
|
+
if (raw.authorized.length === 0) lines.push(" (none)");
|
|
45
|
+
if (raw.pending.length > 0) {
|
|
46
|
+
lines.push("", "Pending:");
|
|
47
|
+
for (const p of raw.pending) lines.push(` ${p.chatId} ${p.name || p.username || ""}`);
|
|
48
|
+
}
|
|
49
|
+
return send(lines.join("\n"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (action === "revoke-pick") {
|
|
53
|
+
const raw = accessStore.listAuthorizedRaw();
|
|
54
|
+
const rows = [];
|
|
55
|
+
for (const a of raw.authorized) {
|
|
56
|
+
if (a.isOwner) continue;
|
|
57
|
+
const person = peopleStore.findByHandle("telegram", a.chatId) || peopleStore.findByHandle("kazee", a.chatId);
|
|
58
|
+
const label = `${a.chatId} ${person ? person.name : (a.name || a.username || "")}`.trim();
|
|
59
|
+
rows.push([{ text: `Revoke ${label}`.slice(0, 60), callback_data: `auth:revoke:${a.chatId}` }]);
|
|
60
|
+
}
|
|
61
|
+
if (rows.length === 0) return send("No revocable chats (only owner is authorized).");
|
|
62
|
+
return send("Pick a chat to revoke:", { keyboard: { inline_keyboard: rows } });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (action === "revoke") {
|
|
66
|
+
const chatId = parts.slice(2).join(":");
|
|
67
|
+
if (!chatId) return;
|
|
68
|
+
const person = peopleStore.findByHandle("telegram", chatId) || peopleStore.findByHandle("kazee", chatId);
|
|
69
|
+
if (person && person.isOwner) return send("Refusing to revoke owner chat.");
|
|
70
|
+
if (person) {
|
|
71
|
+
const detected = (person.handles || []).find((h) => String(h.channelId) === String(chatId));
|
|
72
|
+
if (detected) peopleStore.unlinkHandle(person.id, { adapter: detected.adapter, channelId: detected.channelId });
|
|
73
|
+
}
|
|
74
|
+
const r = accessStore.revokeChat(chatId);
|
|
75
|
+
if (!r.ok) return send(`Revoke failed: ${r.reason}`);
|
|
76
|
+
return send(`Revoked ${chatId}${person ? ` (was ${person.name})` : ""}.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const chatId = parts[2];
|
|
32
80
|
if (!chatId || !["approve", "deny"].includes(action)) return;
|
|
33
81
|
const result = action === "approve" ? approveAuthRequest(chatId) : denyAuthRequest(chatId);
|
|
34
82
|
|
|
@@ -54,6 +102,76 @@ async function handleAction(envelope) {
|
|
|
54
102
|
return;
|
|
55
103
|
}
|
|
56
104
|
|
|
105
|
+
if (d.startsWith("vault:")) {
|
|
106
|
+
const action = d.slice("vault:".length);
|
|
107
|
+
if (action === "lock") {
|
|
108
|
+
vault.lock();
|
|
109
|
+
return send("Vault locked.");
|
|
110
|
+
}
|
|
111
|
+
if (action === "cancel") {
|
|
112
|
+
if (!state.pendingVaultUnlock) return send("Nothing to cancel.");
|
|
113
|
+
state.pendingVaultUnlock = false;
|
|
114
|
+
state.pendingVaultAction = null;
|
|
115
|
+
state.pendingVaultUnlockAt = 0;
|
|
116
|
+
return send("Cancelled.");
|
|
117
|
+
}
|
|
118
|
+
if (action === "create") {
|
|
119
|
+
if (!isChatOwner(envelope.channelId)) return send("Owner only.");
|
|
120
|
+
if (vault.exists()) return send("Vault already exists.");
|
|
121
|
+
state.pendingVaultUnlock = true;
|
|
122
|
+
state.pendingVaultUnlockAt = Date.now();
|
|
123
|
+
state.pendingVaultAction = { type: "create" };
|
|
124
|
+
return send("Pick a vault password and send it next.\n(Message will be deleted. Send /vault cancel to abort.)", {
|
|
125
|
+
keyboard: { inline_keyboard: [[{ text: "Cancel", callback_data: "vault:cancel" }]] },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (action === "hint") {
|
|
129
|
+
return send([
|
|
130
|
+
"Vault usage:",
|
|
131
|
+
" /vault create — set up the vault (owner, first time)",
|
|
132
|
+
" /vault set <name> <value> — save a credential (your message is deleted)",
|
|
133
|
+
" /vault get <name> — reveal a credential",
|
|
134
|
+
" /vault remove <name> — delete a credential",
|
|
135
|
+
" /vault — list (unlocks if needed)",
|
|
136
|
+
" /vault lock — lock now",
|
|
137
|
+
" /vault cancel — abort a pending password prompt",
|
|
138
|
+
].join("\n"));
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (d.startsWith("people:")) {
|
|
144
|
+
const rest = d.slice("people:".length);
|
|
145
|
+
if (rest === "hint") {
|
|
146
|
+
return send([
|
|
147
|
+
"People usage:",
|
|
148
|
+
" /people show <name> — details",
|
|
149
|
+
" /people note <name> <text> — add a note",
|
|
150
|
+
" /people add <name> — owner: add person",
|
|
151
|
+
" /people link <name> <adapter> <channel> — owner: link a chat",
|
|
152
|
+
" /people unlink <name> <adapter> <channel> — owner: unlink",
|
|
153
|
+
" /people set-primary <name> <adapter> <channel>",
|
|
154
|
+
" /people remove <name> — owner",
|
|
155
|
+
].join("\n"));
|
|
156
|
+
}
|
|
157
|
+
if (rest.startsWith("show:")) {
|
|
158
|
+
const id = rest.slice("show:".length);
|
|
159
|
+
const person = peopleStore.findById(id);
|
|
160
|
+
if (!person) return send(`Not found: ${id}`);
|
|
161
|
+
const handles = (person.handles || []).map((h) => ` ${h.adapter}:${h.channelId}${person.primaryChannel && person.primaryChannel.adapter === h.adapter && String(person.primaryChannel.channelId) === String(h.channelId) ? " (primary)" : ""}`).join("\n");
|
|
162
|
+
const notes = (person.notes || []).slice(-5).map((n) => ` - [${(n.at || "").slice(0, 10)}] ${n.text}`).join("\n");
|
|
163
|
+
return send([
|
|
164
|
+
`${person.name}${person.isOwner ? " (owner)" : ""} id=${person.id}`,
|
|
165
|
+
person.bio ? `Bio: ${person.bio}` : null,
|
|
166
|
+
"Handles:",
|
|
167
|
+
handles || " (none)",
|
|
168
|
+
notes ? "Notes:" : null,
|
|
169
|
+
notes || null,
|
|
170
|
+
].filter((x) => x !== null && x !== "").join("\n"));
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
57
175
|
if (d.startsWith("intro:")) {
|
|
58
176
|
if (!isChatOwner(envelope.channelId)) return send("Owner only — intro approvals are restricted.");
|
|
59
177
|
const [, action, introId] = d.split(":");
|
package/core/handlers.js
CHANGED
|
@@ -170,8 +170,15 @@ register({
|
|
|
170
170
|
return send("Usage: /auth | /auth list | /auth revoke <chatId>");
|
|
171
171
|
}
|
|
172
172
|
if (authorized(env)) {
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
if (ownerEnv(env)) {
|
|
174
|
+
return send("You're already authorized.", {
|
|
175
|
+
keyboard: { inline_keyboard: [[
|
|
176
|
+
{ text: "List authorized", callback_data: "auth:list" },
|
|
177
|
+
{ text: "Revoke…", callback_data: "auth:revoke-pick" },
|
|
178
|
+
]] },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return send("You're already authorized.");
|
|
175
182
|
}
|
|
176
183
|
if (!hasOwner()) {
|
|
177
184
|
bootstrapOwner({
|
|
@@ -870,23 +877,40 @@ register({
|
|
|
870
877
|
|
|
871
878
|
// ── Vault ──────────────────────────────────────────────────────────
|
|
872
879
|
|
|
880
|
+
const PENDING_VAULT_TTL_MS = 90 * 1000;
|
|
881
|
+
const PASSWORD_PROMPT_KB = { inline_keyboard: [[{ text: "Cancel", callback_data: "vault:cancel" }]] };
|
|
882
|
+
|
|
883
|
+
function armVaultPrompt(state, action) {
|
|
884
|
+
state.pendingVaultUnlock = true;
|
|
885
|
+
state.pendingVaultUnlockAt = Date.now();
|
|
886
|
+
state.pendingVaultAction = action;
|
|
887
|
+
}
|
|
888
|
+
|
|
873
889
|
register({
|
|
874
|
-
name: "vault", description: "Manage credentials (password required)", args: "[set|remove|lock] ...",
|
|
890
|
+
name: "vault", description: "Manage credentials (password required)", args: "[create|get|set|remove|lock|cancel] ...",
|
|
875
891
|
handler: async (env, { tail }) => {
|
|
876
892
|
if (!authorized(env)) return;
|
|
877
893
|
const state = currentState();
|
|
878
894
|
|
|
879
895
|
if (!tail) {
|
|
880
|
-
if (!vault.exists())
|
|
896
|
+
if (!vault.exists()) {
|
|
897
|
+
if (!ownerEnv(env)) return send("No vault. Ask the owner to run /vault create.");
|
|
898
|
+
return send("No vault yet. Use /vault create to set one up.", {
|
|
899
|
+
keyboard: { inline_keyboard: [[{ text: "Create vault", callback_data: "vault:create" }]] },
|
|
900
|
+
});
|
|
901
|
+
}
|
|
881
902
|
if (vault.isUnlocked()) {
|
|
882
903
|
const entries = vault.list();
|
|
883
904
|
const keys = Object.keys(entries);
|
|
884
|
-
|
|
885
|
-
|
|
905
|
+
const kb = { inline_keyboard: [[
|
|
906
|
+
{ text: "Lock now", callback_data: "vault:lock" },
|
|
907
|
+
{ text: "How to set/get/remove", callback_data: "vault:hint" },
|
|
908
|
+
]] };
|
|
909
|
+
if (keys.length === 0) await send("Vault is unlocked but empty.\n\nUse /vault set <name> <value>", { keyboard: kb });
|
|
910
|
+
else await send("Vault (unlocked):\n\n" + keys.map((k) => `${k}: ${entries[k]}`).join("\n") + "\n\nLocks automatically in 5 min.", { keyboard: kb });
|
|
886
911
|
} else {
|
|
887
|
-
state
|
|
888
|
-
|
|
889
|
-
await send("Vault is locked. Send your vault password.\n(Message will be deleted after reading)");
|
|
912
|
+
armVaultPrompt(state, { type: "list" });
|
|
913
|
+
await send("Vault is locked. Send your vault password.\n(Message will be deleted after reading. Times out in 90s.)", { keyboard: PASSWORD_PROMPT_KB });
|
|
890
914
|
}
|
|
891
915
|
return;
|
|
892
916
|
}
|
|
@@ -896,6 +920,21 @@ register({
|
|
|
896
920
|
return send("Vault locked.");
|
|
897
921
|
}
|
|
898
922
|
|
|
923
|
+
if (tail === "cancel") {
|
|
924
|
+
if (!state.pendingVaultUnlock) return send("Nothing to cancel.");
|
|
925
|
+
state.pendingVaultUnlock = false;
|
|
926
|
+
state.pendingVaultAction = null;
|
|
927
|
+
state.pendingVaultUnlockAt = 0;
|
|
928
|
+
return send("Cancelled.");
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (tail === "create") {
|
|
932
|
+
if (!ownerEnv(env)) return send("Owner only.");
|
|
933
|
+
if (vault.exists()) return send("Vault already exists. Use /vault to unlock, or delete the file first.");
|
|
934
|
+
armVaultPrompt(state, { type: "create" });
|
|
935
|
+
return send("Pick a vault password and send it next.\n(Message will be deleted. Use the button or send /vault cancel to abort.)", { keyboard: PASSWORD_PROMPT_KB });
|
|
936
|
+
}
|
|
937
|
+
|
|
899
938
|
const setMatch = tail.match(/^set\s+(\S+)\s+(.+)$/);
|
|
900
939
|
if (setMatch) {
|
|
901
940
|
await deleteMessage(env.messageId);
|
|
@@ -903,9 +942,19 @@ register({
|
|
|
903
942
|
vault.set(setMatch[1], setMatch[2].trim());
|
|
904
943
|
return send(`Saved: ${setMatch[1]}`);
|
|
905
944
|
}
|
|
906
|
-
state.
|
|
907
|
-
|
|
908
|
-
|
|
945
|
+
armVaultPrompt(state, { type: "set", key: setMatch[1], value: setMatch[2].trim() });
|
|
946
|
+
return send("Vault locked. Send password to unlock.", { keyboard: PASSWORD_PROMPT_KB });
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const getMatch = tail.match(/^get\s+(\S+)$/);
|
|
950
|
+
if (getMatch) {
|
|
951
|
+
if (vault.isUnlocked()) {
|
|
952
|
+
const value = vault.get(getMatch[1]);
|
|
953
|
+
if (value === null) return send(`No such key: ${getMatch[1]}`);
|
|
954
|
+
return send(`${getMatch[1]}: ${value}\n\n(Visible to anyone who reads this chat — delete the message when done.)`);
|
|
955
|
+
}
|
|
956
|
+
armVaultPrompt(state, { type: "get", key: getMatch[1] });
|
|
957
|
+
return send("Vault locked. Send password to unlock.", { keyboard: PASSWORD_PROMPT_KB });
|
|
909
958
|
}
|
|
910
959
|
|
|
911
960
|
const removeMatch = tail.match(/^remove\s+(\S+)$/);
|
|
@@ -914,15 +963,16 @@ register({
|
|
|
914
963
|
vault.remove(removeMatch[1]);
|
|
915
964
|
return send(`Removed: ${removeMatch[1]}`);
|
|
916
965
|
}
|
|
917
|
-
state
|
|
918
|
-
|
|
919
|
-
return send("Vault locked. Send password to unlock.");
|
|
966
|
+
armVaultPrompt(state, { type: "remove", key: removeMatch[1] });
|
|
967
|
+
return send("Vault locked. Send password to unlock.", { keyboard: PASSWORD_PROMPT_KB });
|
|
920
968
|
}
|
|
921
969
|
|
|
922
|
-
send("Usage: /vault | /vault set <name> <value> | /vault remove <name> | /vault lock");
|
|
970
|
+
send("Usage: /vault | /vault create | /vault set <name> <value> | /vault get <name> | /vault remove <name> | /vault lock | /vault cancel");
|
|
923
971
|
},
|
|
924
972
|
});
|
|
925
973
|
|
|
974
|
+
module.exports.PENDING_VAULT_TTL_MS = PENDING_VAULT_TTL_MS;
|
|
975
|
+
|
|
926
976
|
// ── Cron ───────────────────────────────────────────────────────────
|
|
927
977
|
|
|
928
978
|
register({
|
|
@@ -1052,11 +1102,17 @@ register({
|
|
|
1052
1102
|
if (!authorized(env)) return;
|
|
1053
1103
|
if (!tail || tail === "list") {
|
|
1054
1104
|
const all = peopleStore.list();
|
|
1055
|
-
if (all.length === 0)
|
|
1105
|
+
if (all.length === 0) {
|
|
1106
|
+
return send("No people yet.", ownerEnv(env) ? {
|
|
1107
|
+
keyboard: { inline_keyboard: [[{ text: "How to add/link", callback_data: "people:hint" }]] },
|
|
1108
|
+
} : undefined);
|
|
1109
|
+
}
|
|
1056
1110
|
const lines = ["Team:"];
|
|
1057
1111
|
for (const p of all) lines.push(` ${formatPersonShort(p)}`);
|
|
1058
|
-
|
|
1059
|
-
|
|
1112
|
+
const rows = [];
|
|
1113
|
+
for (const p of all) rows.push([{ text: `Show ${p.name}`, callback_data: `people:show:${p.id}` }]);
|
|
1114
|
+
if (ownerEnv(env)) rows.push([{ text: "How to note/link/add", callback_data: "people:hint" }]);
|
|
1115
|
+
return send(lines.join("\n"), { keyboard: { inline_keyboard: rows } });
|
|
1060
1116
|
}
|
|
1061
1117
|
const showMatch = tail.match(/^show\s+(.+)$/i);
|
|
1062
1118
|
if (showMatch) {
|
package/core/router.js
CHANGED
|
@@ -240,18 +240,34 @@ async function handleText(envelope) {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
if (state.pendingVaultUnlock) {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (!ok) {
|
|
243
|
+
const { PENDING_VAULT_TTL_MS } = require("./handlers");
|
|
244
|
+
const armedAt = state.pendingVaultUnlockAt || 0;
|
|
245
|
+
if (PENDING_VAULT_TTL_MS && armedAt && Date.now() - armedAt > PENDING_VAULT_TTL_MS) {
|
|
247
246
|
state.pendingVaultUnlock = false;
|
|
248
247
|
state.pendingVaultAction = null;
|
|
249
|
-
|
|
248
|
+
state.pendingVaultUnlockAt = 0;
|
|
249
|
+
await send("Vault prompt timed out. Re-run the command.");
|
|
250
250
|
return;
|
|
251
251
|
}
|
|
252
|
+
const password = envelope.text;
|
|
253
|
+
await deleteMessage(envelope.messageId);
|
|
252
254
|
const action = state.pendingVaultAction;
|
|
253
255
|
state.pendingVaultUnlock = false;
|
|
254
256
|
state.pendingVaultAction = null;
|
|
257
|
+
state.pendingVaultUnlockAt = 0;
|
|
258
|
+
|
|
259
|
+
if (action && action.type === "create") {
|
|
260
|
+
if (vault.exists()) { await send("Vault was created in another flow — re-run /vault."); return; }
|
|
261
|
+
vault.create(password);
|
|
262
|
+
await send("Vault created and unlocked.\n\nUse /vault set <name> <value>");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const ok = vault.unlock(password);
|
|
267
|
+
if (!ok) {
|
|
268
|
+
await send("Wrong password.");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
255
271
|
if (action.type === "list") {
|
|
256
272
|
const entries = vault.list();
|
|
257
273
|
const keys = Object.keys(entries);
|
|
@@ -263,6 +279,10 @@ async function handleText(envelope) {
|
|
|
263
279
|
} else if (action.type === "remove") {
|
|
264
280
|
vault.remove(action.key);
|
|
265
281
|
await send(`Removed: ${action.key}`);
|
|
282
|
+
} else if (action.type === "get") {
|
|
283
|
+
const value = vault.get(action.key);
|
|
284
|
+
if (value === null) await send(`No such key: ${action.key}`);
|
|
285
|
+
else await send(`${action.key}: ${value}\n\n(Visible to anyone who reads this chat — delete the message when done.)`);
|
|
266
286
|
}
|
|
267
287
|
return;
|
|
268
288
|
}
|
package/package.json
CHANGED
package/vault.js
CHANGED
|
@@ -23,7 +23,9 @@ class Vault {
|
|
|
23
23
|
// Create a new vault with a password
|
|
24
24
|
create(password) {
|
|
25
25
|
this.data = {};
|
|
26
|
+
this._password = password;
|
|
26
27
|
this._save(password);
|
|
28
|
+
this._resetLockTimer();
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
// Derive key from password + salt
|
|
@@ -125,7 +127,7 @@ class Vault {
|
|
|
125
127
|
const entries = {};
|
|
126
128
|
for (const [k, v] of Object.entries(this.data)) {
|
|
127
129
|
const val = String(v);
|
|
128
|
-
entries[k] = val.length
|
|
130
|
+
entries[k] = val.length >= 4 ? "****" + val.slice(-4) : "****";
|
|
129
131
|
}
|
|
130
132
|
return entries;
|
|
131
133
|
}
|