viber-channel 0.8.3 → 0.8.4
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/lib/auth.ts +23 -6
- package/lib/bridge_core.ts +4 -1
- package/lib/instance.ts +113 -36
- package/package.json +1 -1
- package/viber-channel.ts +19 -2
- package/viber-codex-bridge.ts +6 -1
package/lib/auth.ts
CHANGED
|
@@ -33,21 +33,38 @@ export interface AuthJson {
|
|
|
33
33
|
instance_id?: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The environment signal — SINGLE source of truth for every env-dependent
|
|
38
|
+
* behavior in the channel (auth file, agent-session yield, …). The DEV backend
|
|
39
|
+
* is the only non-default: anything else (unset / staging / prod) uses the
|
|
40
|
+
* STAGING defaults, so a missing or ambiguous signal can never accidentally
|
|
41
|
+
* pull dev behavior into staging. Derived from `VIBER_BASE_URL`, which the
|
|
42
|
+
* launcher always sets per env.
|
|
43
|
+
*/
|
|
44
|
+
export function isDevBackend(
|
|
45
|
+
baseUrl: string = process.env.VIBER_BASE_URL ?? "",
|
|
46
|
+
): boolean {
|
|
47
|
+
return baseUrl.includes("viber-dev");
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
/**
|
|
37
51
|
* Resolve the auth file path.
|
|
38
52
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
53
|
+
* The optional `VIBER_AUTH_FILE` env var overrides everything — absolute paths
|
|
54
|
+
* verbatim, relative paths resolved against `cwd`. With no override the file is
|
|
55
|
+
* DERIVED from the environment (single source of truth, {@link isDevBackend}):
|
|
56
|
+
* the dev backend uses `.viber/dev.auth.json`, everything else defaults to
|
|
57
|
+
* `.viber/auth.json` (staging). This kills the class of bugs where a dev channel
|
|
58
|
+
* silently authenticated against the dev backend with STAGING creds (`-32000`)
|
|
59
|
+
* because only `VIBER_BASE_URL` was set and the auth file defaulted to staging.
|
|
44
60
|
*/
|
|
45
61
|
export function authFilePath(cwd: string = process.cwd()): string {
|
|
46
62
|
const override = process.env.VIBER_AUTH_FILE;
|
|
47
63
|
if (override !== undefined && override.trim() !== "") {
|
|
48
64
|
return isAbsolute(override) ? override : join(cwd, override);
|
|
49
65
|
}
|
|
50
|
-
|
|
66
|
+
const file = isDevBackend() ? "dev.auth.json" : "auth.json";
|
|
67
|
+
return join(cwd, ".viber", file);
|
|
51
68
|
}
|
|
52
69
|
|
|
53
70
|
export function loadAuth(cwd: string = process.cwd()): AuthJson {
|
package/lib/bridge_core.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { buildSseUrl } from "./urls.js";
|
|
|
27
27
|
import { cfAccessHeaders } from "./cfAccess.js";
|
|
28
28
|
import { sessionFilePath, writeHandle } from "./channel_session.js";
|
|
29
29
|
import type { AuthJson } from "./auth.js";
|
|
30
|
-
import { acquireInstance, registerInstance, instanceKindFromEnv, type AcquiredInstance } from "./instance.js";
|
|
30
|
+
import { acquireInstance, registerInstance, instanceKindFromEnv, maybeAttachTeam, type AcquiredInstance } from "./instance.js";
|
|
31
31
|
import {
|
|
32
32
|
runPersistentControlStream,
|
|
33
33
|
sendInstanceHeartbeat,
|
|
@@ -897,6 +897,9 @@ export async function acquireBridgeInstance(opts: {
|
|
|
897
897
|
process.stderr.write(`${logPrefix} instance: registering fresh bridge instance\n`);
|
|
898
898
|
const reg = await registerInstance(baseUrl, auth.project_id, auth.project_token, fingerprint, label, instanceKindFromEnv() ?? kind);
|
|
899
899
|
process.stderr.write(`${logPrefix} instance: registered fresh bridge instance (id=${reg.instance_id})\n`);
|
|
900
|
+
// #398: best-effort self-attach to VIBER_INSTANCE_TEAM (codex/gemma path; the
|
|
901
|
+
// two branches above go through acquireInstance which already self-attaches).
|
|
902
|
+
await maybeAttachTeam(baseUrl, auth.project_id, auth.project_token, fingerprint, reg.instance_id, (m) => process.stderr.write(m));
|
|
900
903
|
return { instance_token: reg.instance_token, instance_key: reg.instance_id };
|
|
901
904
|
}
|
|
902
905
|
|
package/lib/instance.ts
CHANGED
|
@@ -76,6 +76,77 @@ export function instanceKindFromEnv(): string | undefined {
|
|
|
76
76
|
return raw !== undefined && raw.trim() !== "" ? raw.trim() : undefined;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The DB-backed team this process should join (#398), from `VIBER_INSTANCE_TEAM`
|
|
81
|
+
* (set by the spawn path — vibe-master's agent_spec). undefined when unset → the
|
|
82
|
+
* agent joins no team. Mirrors `instanceKindFromEnv`.
|
|
83
|
+
*/
|
|
84
|
+
export function instanceTeamFromEnv(): string | undefined {
|
|
85
|
+
const raw = process.env.VIBER_INSTANCE_TEAM;
|
|
86
|
+
return raw !== undefined && raw.trim() !== "" ? raw.trim() : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* POST /api/projects/:id/teams/members — attach this instance to a team by name
|
|
91
|
+
* (#398). A SEPARATE authorized call AFTER register (the register payload never
|
|
92
|
+
* carries a client-declared team — same discipline as the orchestrator flag).
|
|
93
|
+
* Team is project-scoped metadata; the worker upserts it and verifies the
|
|
94
|
+
* instance belongs to the token's project. Throws InstanceRegisterError on !ok.
|
|
95
|
+
*/
|
|
96
|
+
export async function attachInstanceTeam(
|
|
97
|
+
baseUrl: string,
|
|
98
|
+
projectId: number,
|
|
99
|
+
projectToken: string,
|
|
100
|
+
fingerprint: string,
|
|
101
|
+
instanceId: string,
|
|
102
|
+
team: string,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const resp = await fetch(
|
|
105
|
+
`${baseUrl}/api/projects/${projectId}/teams/members`,
|
|
106
|
+
{
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: {
|
|
109
|
+
Authorization: `Bearer ${projectToken}`,
|
|
110
|
+
"X-Client-Fingerprint": fingerprint,
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
...cfAccessHeaders(),
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify({ team, instance_id: instanceId }),
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
if (!resp.ok) {
|
|
118
|
+
const detail = await resp.text().catch(() => "");
|
|
119
|
+
throw new InstanceRegisterError(resp.status, detail);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Best-effort self-attach to `VIBER_INSTANCE_TEAM`, run AFTER a successful
|
|
125
|
+
* register/acquire (#398, R-CLAUDE: called from BOTH acquireInstance — claude/
|
|
126
|
+
* plain — and acquireBridgeInstance — codex/gemma). No-op when no team is
|
|
127
|
+
* requested or no server instance_id is known. A failure (legacy worker 404,
|
|
128
|
+
* validation, network) is logged and swallowed — the agent still works,
|
|
129
|
+
* un-teamed (forward-compatible). NOT retried.
|
|
130
|
+
*/
|
|
131
|
+
export async function maybeAttachTeam(
|
|
132
|
+
baseUrl: string,
|
|
133
|
+
projectId: number,
|
|
134
|
+
projectToken: string,
|
|
135
|
+
fingerprint: string,
|
|
136
|
+
instanceId: string | undefined,
|
|
137
|
+
log: (msg: string) => void = (m) => process.stderr.write(m),
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
const team = instanceTeamFromEnv();
|
|
140
|
+
if (team === undefined || instanceId === undefined) return;
|
|
141
|
+
try {
|
|
142
|
+
await attachInstanceTeam(baseUrl, projectId, projectToken, fingerprint, instanceId, team);
|
|
143
|
+
log(`[viber-channel] team: attached instance ${instanceId.slice(0, 8)} to team "${team}"\n`);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
146
|
+
log(`[viber-channel] team: self-attach to "${team}" failed (non-fatal): ${msg}\n`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
79
150
|
export interface AcquiredInstance {
|
|
80
151
|
instance_token: string;
|
|
81
152
|
/**
|
|
@@ -116,6 +187,10 @@ export async function acquireInstance(
|
|
|
116
187
|
log: (msg: string) => void = (m) => process.stderr.write(m),
|
|
117
188
|
cwd: string = process.cwd(),
|
|
118
189
|
): Promise<AcquiredInstance> {
|
|
190
|
+
// Resolve the identity into `result` (one of the four paths), then run the
|
|
191
|
+
// #398 team self-attach ONCE at the single exit — covers every path (incl. the
|
|
192
|
+
// claude/plain register paths) without scattering the call (R-CLAUDE).
|
|
193
|
+
let result: AcquiredInstance;
|
|
119
194
|
const envToken = process.env.VIBER_INSTANCE_TOKEN;
|
|
120
195
|
if (envToken !== undefined && envToken.trim() !== "") {
|
|
121
196
|
const token = envToken.trim();
|
|
@@ -123,42 +198,44 @@ export async function acquireInstance(
|
|
|
123
198
|
const realId = envId !== undefined && envId.trim() !== "" ? envId.trim() : undefined;
|
|
124
199
|
const key = realId ?? token;
|
|
125
200
|
log(`[viber-channel] instance: using VIBER_INSTANCE_TOKEN (env-injected)\n`);
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
201
|
+
result = { instance_token: token, instance_key: key, instance_id: realId };
|
|
202
|
+
} else {
|
|
203
|
+
const envLabel = process.env.VIBER_CHANNEL_LABEL;
|
|
204
|
+
if (envLabel !== undefined && envLabel.trim() !== "") {
|
|
205
|
+
const label = envLabel.trim();
|
|
206
|
+
log(`[viber-channel] instance: registering fresh labelled instance "${label}"\n`);
|
|
207
|
+
const reg = await registerInstance(
|
|
208
|
+
baseUrl,
|
|
209
|
+
auth.project_id,
|
|
210
|
+
auth.project_token,
|
|
211
|
+
fingerprint,
|
|
212
|
+
label,
|
|
213
|
+
instanceKindFromEnv(),
|
|
214
|
+
);
|
|
215
|
+
result = { instance_token: reg.instance_token, instance_key: reg.instance_id, instance_id: reg.instance_id };
|
|
216
|
+
} else if (auth.instance_token !== undefined && auth.instance_token.trim() !== "") {
|
|
217
|
+
const realId =
|
|
218
|
+
auth.instance_id !== undefined && auth.instance_id.trim() !== "" ? auth.instance_id : undefined;
|
|
219
|
+
const key = realId ?? auth.instance_token;
|
|
220
|
+
log(`[viber-channel] instance: reusing persisted instance_token (id=${auth.instance_id ?? "?"})\n`);
|
|
221
|
+
result = { instance_token: auth.instance_token, instance_key: key, instance_id: realId };
|
|
222
|
+
} else {
|
|
223
|
+
log(`[viber-channel] instance: none found — registering a new one via the project_token\n`);
|
|
224
|
+
const reg = await registerInstance(
|
|
225
|
+
baseUrl,
|
|
226
|
+
auth.project_id,
|
|
227
|
+
auth.project_token,
|
|
228
|
+
fingerprint,
|
|
229
|
+
undefined,
|
|
230
|
+
instanceKindFromEnv(),
|
|
231
|
+
);
|
|
232
|
+
persistInstanceCredentials(authFilePath(cwd), reg.instance_id, reg.instance_token, log);
|
|
233
|
+
log(`[viber-channel] instance: registered + persisted (id=${reg.instance_id})\n`);
|
|
234
|
+
result = { instance_token: reg.instance_token, instance_key: reg.instance_id, instance_id: reg.instance_id };
|
|
235
|
+
}
|
|
150
236
|
}
|
|
151
237
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
auth.project_id,
|
|
156
|
-
auth.project_token,
|
|
157
|
-
fingerprint,
|
|
158
|
-
undefined,
|
|
159
|
-
instanceKindFromEnv(),
|
|
160
|
-
);
|
|
161
|
-
persistInstanceCredentials(authFilePath(cwd), reg.instance_id, reg.instance_token, log);
|
|
162
|
-
log(`[viber-channel] instance: registered + persisted (id=${reg.instance_id})\n`);
|
|
163
|
-
return { instance_token: reg.instance_token, instance_key: reg.instance_id, instance_id: reg.instance_id };
|
|
238
|
+
// #398: best-effort self-attach to VIBER_INSTANCE_TEAM (no-op when unset).
|
|
239
|
+
await maybeAttachTeam(baseUrl, auth.project_id, auth.project_token, fingerprint, result.instance_id, log);
|
|
240
|
+
return result;
|
|
164
241
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viber-channel",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Voice + text MCP channel between a Claude Code session and the Viber UI (https://viber.dgypx.dev). Push transcripts to Claude; send_message tool delivers text back to the UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/viber-channel.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from "./lib/conversation.ts";
|
|
43
43
|
import { startInstanceHeartbeat } from "./lib/heartbeat.ts";
|
|
44
44
|
import { startParentWatchdog } from "./lib/parent_watchdog.ts";
|
|
45
|
-
import { acquireInstance, registerInstance } from "./lib/instance.ts";
|
|
45
|
+
import { acquireInstance, maybeAttachTeam, registerInstance } from "./lib/instance.ts";
|
|
46
46
|
import {
|
|
47
47
|
runPersistentControlStream,
|
|
48
48
|
sendInstanceHeartbeat,
|
|
@@ -71,7 +71,17 @@ import { capabilitiesText } from "./lib/capabilities.ts";
|
|
|
71
71
|
// loads in the same session with the same env; instead of racing it for the
|
|
72
72
|
// channel lock, it bows out deterministically here. Normal sessions
|
|
73
73
|
// (no VIBER_CHANNEL_AGENT_SESSION) are unaffected.
|
|
74
|
+
//
|
|
75
|
+
// BUT the yield is a STAGING/PROD-only behavior: the plugin channel only runs
|
|
76
|
+
// there (it is disabled in the Viber dev repo, #365). On the DEV backend there
|
|
77
|
+
// is NO plugin to yield to, so yielding = no channel at all (`-32000`, no
|
|
78
|
+
// registration). Derive this from the environment (single source of truth,
|
|
79
|
+
// isDevBackend) instead of relying on VIBER_CHANNEL_AGENT_SESSION not leaking
|
|
80
|
+
// from the parent shell into the spawned dev terminal (the real bug: that var
|
|
81
|
+
// leaked and terminal.ts never cleared it, so the dev channel silently bowed
|
|
82
|
+
// out). Default (non-dev) behavior is unchanged.
|
|
74
83
|
if (
|
|
84
|
+
!isDevBackend() &&
|
|
75
85
|
process.env.VIBER_CHANNEL_AGENT_SESSION === "1" &&
|
|
76
86
|
process.env.VIBER_CHANNEL_PLUGIN !== "1"
|
|
77
87
|
) {
|
|
@@ -307,7 +317,12 @@ process.stdin.on("end", () => {
|
|
|
307
317
|
|
|
308
318
|
// ---- Auth from .viber/auth.json ----
|
|
309
319
|
|
|
310
|
-
import {
|
|
320
|
+
import {
|
|
321
|
+
authFilePath,
|
|
322
|
+
isDevBackend,
|
|
323
|
+
loadAuth,
|
|
324
|
+
persistInstanceCredentials,
|
|
325
|
+
} from "./lib/auth.ts";
|
|
311
326
|
import { handleConversationTokenExpired } from "./lib/channel_errors.ts";
|
|
312
327
|
import { buildSseUrl } from "./lib/urls.ts";
|
|
313
328
|
|
|
@@ -678,6 +693,8 @@ try {
|
|
|
678
693
|
persistInstanceCredentials(authFilePath(), reg.instance_id, reg.instance_token);
|
|
679
694
|
instanceToken = reg.instance_token;
|
|
680
695
|
OWN_INSTANCE_ID = reg.instance_id;
|
|
696
|
+
// #398: re-attach the fresh (post-revocation) instance to its team.
|
|
697
|
+
await maybeAttachTeam(BASE_URL, auth.project_id, auth.project_token, auth.client_fingerprint, reg.instance_id);
|
|
681
698
|
SESSION_FILE = sessionFilePath(LOCK_BASE_URL, reg.instance_id, LOCK_DIR);
|
|
682
699
|
minted = await acquireConversation(
|
|
683
700
|
SESSION_FILE,
|
package/viber-codex-bridge.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "./lib/conversation.ts";
|
|
19
19
|
import { sessionFilePath, writeHandle } from "./lib/channel_session.ts";
|
|
20
20
|
import { clientFingerprint } from "./lib/fingerprint.ts";
|
|
21
|
-
import { acquireInstance, instanceKindFromEnv, registerInstance } from "./lib/instance.ts";
|
|
21
|
+
import { acquireInstance, instanceKindFromEnv, maybeAttachTeam, registerInstance } from "./lib/instance.ts";
|
|
22
22
|
import {
|
|
23
23
|
acquireBridgeLock as coreAcquireBridgeLock,
|
|
24
24
|
agentIdentityFromEnv,
|
|
@@ -820,6 +820,9 @@ async function acquireBridgeInstance(
|
|
|
820
820
|
process.stderr.write("[viber-codex-bridge] instance: registering fresh bridge instance\n");
|
|
821
821
|
const reg = await registerInstance(baseUrl, auth.project_id, auth.project_token, fingerprint, label, instanceKindFromEnv() ?? "codex");
|
|
822
822
|
process.stderr.write(`[viber-codex-bridge] instance: registered fresh bridge instance (id=${reg.instance_id})\n`);
|
|
823
|
+
// #398 (R-CODEX-FRESH): self-attach here too so codex is teamed structurally,
|
|
824
|
+
// not only via the VIBER_INSTANCE_TOKEN branch above (parity with claude).
|
|
825
|
+
await maybeAttachTeam(baseUrl, auth.project_id, auth.project_token, fingerprint, reg.instance_id);
|
|
823
826
|
return { instance_token: reg.instance_token, instance_key: reg.instance_id };
|
|
824
827
|
}
|
|
825
828
|
|
|
@@ -885,6 +888,8 @@ async function acquireTargetConversation(
|
|
|
885
888
|
const reg = await registerInstance(BASE_URL, auth.project_id, auth.project_token, fingerprint, label, instanceKindFromEnv() ?? "codex");
|
|
886
889
|
instanceToken = reg.instance_token;
|
|
887
890
|
instanceKey = reg.instance_id;
|
|
891
|
+
// #398: re-attach the fresh (post-revocation) codex instance to its team.
|
|
892
|
+
await maybeAttachTeam(BASE_URL, auth.project_id, auth.project_token, fingerprint, reg.instance_id);
|
|
888
893
|
// Identity changed → re-key the lock onto the new instance. Acquire the new
|
|
889
894
|
// lock BEFORE releasing the old one: if acquisition throws, the old lock is
|
|
890
895
|
// still held and the outer catch releases exactly it (no dangling reference).
|