arisa 5.2.21 → 5.2.22
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/package.json
CHANGED
|
@@ -33,6 +33,15 @@ export function getPiAuthIssue(error) {
|
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function buildPiRequestFailureMessage(error) {
|
|
37
|
+
const message = getErrorMessage(error);
|
|
38
|
+
const limited = /usage limit|quota|rate[ _-]?limit|too many requests|insufficient[_ ]credits|credits? (?:exhausted|depleted)/i.test(message);
|
|
39
|
+
const title = limited
|
|
40
|
+
? "The provider's usage quota or request limit was reached. Retry when capacity is available; signing in again does not restore quota."
|
|
41
|
+
: "Pi request could not be completed. This does not establish an authentication failure.";
|
|
42
|
+
return `${title}\nDetails: ${message}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
export async function getPiAuthStatus(config, chatId = null) {
|
|
37
46
|
const runtime = await createPiRuntime({
|
|
38
47
|
provider: config.pi.provider,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildPiAuthRecoveryBlockedMessage,
|
|
3
3
|
buildPiAuthTelegramMessage,
|
|
4
|
+
buildPiRequestFailureMessage,
|
|
4
5
|
getErrorMessage,
|
|
5
6
|
getPiAuthIssue,
|
|
6
7
|
getPiAuthStatus
|
|
@@ -37,12 +38,16 @@ export function createTelegramAuthController({
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function rememberValidationFailure(error) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
// Quota, network and server failures must never latch the Telegram auth gate.
|
|
42
|
+
issue = getPiAuthIssue(error);
|
|
43
|
+
return issue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function buildValidationFailureMessage(chatId, error) {
|
|
47
|
+
const detected = rememberValidationFailure(error);
|
|
48
|
+
return detected
|
|
49
|
+
? buildPiAuthTelegramMessage({ config, chatId, issue: detected })
|
|
50
|
+
: buildPiRequestFailureMessage(error);
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
async function notifyIssueIfNeeded(chatId, error) {
|
|
@@ -62,15 +67,14 @@ export function createTelegramAuthController({
|
|
|
62
67
|
async function finishRenewal(chatId, renewal) {
|
|
63
68
|
try {
|
|
64
69
|
await renewal.promise;
|
|
65
|
-
await agentManager.validateAgent();
|
|
66
|
-
agentManager.clearSessionCache(chatId);
|
|
67
70
|
issue = null;
|
|
71
|
+
agentManager.clearSessionCache(chatId);
|
|
72
|
+
await agentManager.validateAgent();
|
|
68
73
|
logger?.log("telegram", `Pi auth renewal completed for chat ${chatId}`);
|
|
69
74
|
await api.sendMessage(chatId, await buildPiAuthTelegramMessage({ config, chatId, verified: true }));
|
|
70
75
|
} catch (error) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
await api.sendMessage(chatId, await buildPiAuthTelegramMessage({ config, chatId, issue: detected })).catch((notifyError) => {
|
|
76
|
+
logger?.error("telegram", `Pi auth renewal or validation failed for chat ${chatId}: ${getErrorMessage(error)}`);
|
|
77
|
+
await api.sendMessage(chatId, await buildValidationFailureMessage(chatId, error)).catch((notifyError) => {
|
|
74
78
|
logger?.error("telegram", `auth renewal failure notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
|
|
75
79
|
});
|
|
76
80
|
} finally {
|
|
@@ -141,7 +145,9 @@ export function createTelegramAuthController({
|
|
|
141
145
|
if (!authorization.ok) return;
|
|
142
146
|
|
|
143
147
|
const status = await getPiAuthStatus(config, ctx.chat.id);
|
|
144
|
-
|
|
148
|
+
const canValidateStoredAuth = status.hasStoredAuth && !issue && !renewals.has(chatKey(ctx.chat.id));
|
|
149
|
+
// A stored OAuth login may still be valid even when the provider has no quota.
|
|
150
|
+
if (status.hasApiKey || !status.supportsOAuth || canValidateStoredAuth) {
|
|
145
151
|
await withTyping(ctx, async () => {
|
|
146
152
|
try {
|
|
147
153
|
await agentManager.validateAgent();
|
|
@@ -149,8 +155,7 @@ export function createTelegramAuthController({
|
|
|
149
155
|
issue = null;
|
|
150
156
|
await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, verified: true }));
|
|
151
157
|
} catch (error) {
|
|
152
|
-
|
|
153
|
-
await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
|
|
158
|
+
await ctx.reply(await buildValidationFailureMessage(ctx.chat.id, error));
|
|
154
159
|
}
|
|
155
160
|
});
|
|
156
161
|
return;
|
|
@@ -162,8 +167,7 @@ export function createTelegramAuthController({
|
|
|
162
167
|
? "Starting Pi login from Telegram..."
|
|
163
168
|
: "Pi login is already in progress. Paste the redirect URL or code here when you have it.");
|
|
164
169
|
} catch (error) {
|
|
165
|
-
|
|
166
|
-
await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
|
|
170
|
+
await ctx.reply(await buildValidationFailureMessage(ctx.chat.id, error));
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { createTelegramAuthController } from "../src/transport/telegram/telegram-auth-controller.js";
|
|
5
|
+
|
|
6
|
+
function setup(t, { stored = true, apiKey = "", failure = null, loginFailure = null } = {}) {
|
|
7
|
+
const messages = [];
|
|
8
|
+
const calls = { login: 0, validate: 0, clear: 0 };
|
|
9
|
+
t.mock.method(ModelRuntime, "create", async () => ({
|
|
10
|
+
setRuntimeApiKey: async () => {},
|
|
11
|
+
getProviderAuthStatus: () => ({ configured: stored, source: "stored" }),
|
|
12
|
+
getProvider: () => ({ auth: { oauth: {} } }),
|
|
13
|
+
async login() {
|
|
14
|
+
calls.login++;
|
|
15
|
+
if (loginFailure) throw loginFailure;
|
|
16
|
+
return { type: "oauth", access: "test-only" };
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
const controller = createTelegramAuthController({
|
|
20
|
+
config: { pi: { provider: "openai-codex", model: "test-model", apiKey } },
|
|
21
|
+
api: { sendMessage: async (_chatId, text) => messages.push(text) },
|
|
22
|
+
agentManager: {
|
|
23
|
+
async validateAgent() {
|
|
24
|
+
calls.validate++;
|
|
25
|
+
if (failure) throw failure;
|
|
26
|
+
},
|
|
27
|
+
clearSessionCache() { calls.clear++; }
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
const command = () => controller.handleCommand({
|
|
31
|
+
chat: { id: 123 }, reply: async (text) => messages.push(text)
|
|
32
|
+
}, { authorize: async () => ({ ok: true }), withTyping: async (_ctx, fn) => fn() });
|
|
33
|
+
return { controller, messages, calls, command };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const apiKey of ["", "test-key"]) {
|
|
37
|
+
for (const message of [
|
|
38
|
+
"Codex error: The usage limit has been reached",
|
|
39
|
+
"429 Too many requests",
|
|
40
|
+
"insufficient_quota",
|
|
41
|
+
"request timed out",
|
|
42
|
+
"503 service unavailable"
|
|
43
|
+
]) {
|
|
44
|
+
test(`validation preserves usable auth after ${message} (${apiKey ? "API key" : "OAuth"})`, async (t) => {
|
|
45
|
+
const { controller, calls, command, messages } = setup(t, { apiKey, failure: new Error(message) });
|
|
46
|
+
await command();
|
|
47
|
+
assert.equal(controller.getIssue(), null);
|
|
48
|
+
assert.equal(calls.login, 0);
|
|
49
|
+
assert.equal(calls.clear, 0);
|
|
50
|
+
assert.match(messages[0], new RegExp(message));
|
|
51
|
+
assert.doesNotMatch(messages[0], /Send \/auth|Run \/auth|Update the key|not ready/);
|
|
52
|
+
await command();
|
|
53
|
+
assert.equal(calls.validate, 2);
|
|
54
|
+
assert.equal(calls.login, 0);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("/auth validates stored OAuth credentials instead of starting a device login", async (t) => {
|
|
60
|
+
const { controller, calls, command, messages } = setup(t);
|
|
61
|
+
await command();
|
|
62
|
+
assert.equal(calls.validate, 1);
|
|
63
|
+
assert.equal(calls.login, 0);
|
|
64
|
+
assert.equal(controller.getIssue(), null);
|
|
65
|
+
assert.match(messages[0], /authentication is working/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("real token invalidation remains blocking and permits OAuth recovery", async (t) => {
|
|
69
|
+
const { controller, command, calls, messages } = setup(t, { failure: new Error("auth token revoked") });
|
|
70
|
+
await command();
|
|
71
|
+
assert.equal(controller.getIssue()?.kind, "invalidated-token");
|
|
72
|
+
assert.match(messages[0], /Run \/auth/);
|
|
73
|
+
await command();
|
|
74
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
75
|
+
assert.equal(calls.login, 1);
|
|
76
|
+
assert.equal(controller.getIssue()?.kind, "invalidated-token");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("successful OAuth renewal followed by quota failure clears a stale auth block", async (t) => {
|
|
80
|
+
const { controller, command, calls, messages } = setup(t, {
|
|
81
|
+
failure: new Error("Codex error: The usage limit has been reached")
|
|
82
|
+
});
|
|
83
|
+
controller.rememberIssue(new Error("auth token expired"));
|
|
84
|
+
await command();
|
|
85
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
86
|
+
assert.equal(calls.login, 1);
|
|
87
|
+
assert.equal(calls.clear, 1);
|
|
88
|
+
assert.equal(controller.getIssue(), null);
|
|
89
|
+
assert.equal(controller.hasActiveRenewal(123), false);
|
|
90
|
+
assert.match(messages.join("\n"), /signing in again does not restore quota/);
|
|
91
|
+
await command();
|
|
92
|
+
assert.equal(calls.login, 1);
|
|
93
|
+
assert.equal(calls.validate, 2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("missing credentials still initiate login", async (t) => {
|
|
97
|
+
const { controller, command, calls } = setup(t, { stored: false });
|
|
98
|
+
await command();
|
|
99
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
100
|
+
assert.equal(calls.login, 1);
|
|
101
|
+
assert.equal(calls.validate, 1);
|
|
102
|
+
assert.equal(controller.getIssue(), null);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("transient OAuth login failure does not create an authentication block", async (t) => {
|
|
106
|
+
const { controller, command, calls, messages } = setup(t, {
|
|
107
|
+
stored: false, loginFailure: new Error("request timed out")
|
|
108
|
+
});
|
|
109
|
+
await command();
|
|
110
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
111
|
+
assert.equal(calls.validate, 0);
|
|
112
|
+
assert.equal(controller.getIssue(), null);
|
|
113
|
+
assert.equal(controller.hasActiveRenewal(123), false);
|
|
114
|
+
assert.match(messages.join("\n"), /does not establish an authentication failure/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("ordinary prompt quota errors never latch the auth issue", async (t) => {
|
|
118
|
+
const { controller, messages } = setup(t);
|
|
119
|
+
assert.equal(await controller.notifyIssueIfNeeded(123, new Error("Codex error: The usage limit has been reached")), false);
|
|
120
|
+
assert.equal(controller.getIssue(), null);
|
|
121
|
+
assert.equal(messages.length, 0);
|
|
122
|
+
assert.equal(await controller.notifyIssueIfNeeded(123, new Error("No auth found")), true);
|
|
123
|
+
assert.equal(controller.getIssue()?.kind, "missing-auth");
|
|
124
|
+
});
|