Skip to content

Commit 1a773c1

Browse files
committed
[Bug] AI features failing with 401 "No cookie auth credentials found" #5
1 parent db5019b commit 1a773c1

14 files changed

Lines changed: 191 additions & 68 deletions

File tree

apps/backend/pb_hooks/cron_exchange.pb.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
// main currency: stored_rate[X] = eurRates[X] / eurRates[mainCode]
99
// ================================================================
1010
cronAdd("updateExchange", "0 0,12 * * *", () => {
11-
const fixerRecords = $app.findRecordsByFilter("fixer_settings", "api_key != ''", "", 0, 0);
11+
// NOTE: api_key is a hidden field (migration 0017) — PocketBase silently drops hidden
12+
// fields from filter expressions. Fetch all fixer_settings and validate the key in JS.
13+
const fixerRecords = $app.findRecordsByFilter("fixer_settings", "1=1", "", 0, 0);
1214

1315
for (const fixer of fixerRecords) {
1416
const apiKey = fixer.get("api_key");
17+
// Skip records where no API key has been stored yet
18+
if (!String(apiKey || "").trim()) continue;
1519
const provider = fixer.get("provider") || "fixer";
1620
const userId = fixer.get("user");
1721

apps/backend/pb_hooks/lib/pure/ai-parsers.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,26 +42,35 @@ function buildRecommendationRequest(rawUrl, apiKey, model, systemPrompt, userPro
4242
const isGemini = detectGeminiUrl(rawUrl);
4343

4444
if (isGemini) {
45+
const aiHeaders = { "Content-Type": "application/json" };
46+
if (apiKey) {
47+
aiHeaders["x-goog-api-key"] = apiKey;
48+
}
49+
4550
return {
4651
isGemini: true,
4752
aiUrl: rawUrl + "/models/" + (model || "gemini-1.5-flash") + ":generateContent",
48-
aiHeaders: {
49-
"Content-Type": "application/json",
50-
...(apiKey ? { "x-goog-api-key": apiKey } : {}),
51-
},
53+
aiHeaders: aiHeaders,
5254
aiBody: {
5355
contents: [{ parts: [{ text: systemPrompt + "\n\n" + userPrompt }] }],
5456
},
5557
};
5658
}
5759

60+
const aiHeaders = { "Content-Type": "application/json" };
61+
if (apiKey) {
62+
aiHeaders["Authorization"] = "Bearer " + apiKey;
63+
}
64+
// OpenRouter requires these headers for all requests, especially on free-tier models.
65+
if (rawUrl && rawUrl.indexOf("openrouter.ai") !== -1) {
66+
aiHeaders["HTTP-Referer"] = "https://github.com/danielalves96/zublo";
67+
aiHeaders["X-Title"] = "Zublo";
68+
}
69+
5870
return {
5971
isGemini: false,
6072
aiUrl: rawUrl + "/chat/completions",
61-
aiHeaders: {
62-
"Content-Type": "application/json",
63-
...(apiKey ? { Authorization: "Bearer " + apiKey } : {}),
64-
},
73+
aiHeaders: aiHeaders,
6574
aiBody: {
6675
model: model || "",
6776
messages: [

apps/backend/pb_hooks/lib/pure/chat-ai.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,23 @@ function buildChatRequest(rawUrl, apiKey, model, messages, tools) {
110110
openAiBody.tool_choice = "auto";
111111
}
112112

113+
// Build headers explicitly — object spread (`...`) is not reliably supported in the
114+
// PocketBase Goja runtime and can silently produce empty header maps, causing 401s.
115+
var openAiHeaders = { "Content-Type": "application/json" };
116+
if (apiKey) {
117+
openAiHeaders["Authorization"] = "Bearer " + apiKey;
118+
}
119+
// OpenRouter requires these headers for all requests, especially on free-tier models.
120+
// Without them the API responds with 401 even when the key itself is valid.
121+
if (rawUrl && rawUrl.indexOf("openrouter.ai") !== -1) {
122+
openAiHeaders["HTTP-Referer"] = "https://github.com/danielalves96/zublo";
123+
openAiHeaders["X-Title"] = "Zublo";
124+
}
125+
113126
return {
114127
isGemini: false,
115128
url: rawUrl + "/chat/completions",
116-
headers: {
117-
"Content-Type": "application/json",
118-
...(apiKey ? { Authorization: "Bearer " + apiKey } : {}),
119-
},
129+
headers: openAiHeaders,
120130
body: openAiBody,
121131
};
122132
}

apps/backend/pb_hooks/routes_chat.pb.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,15 @@ routerAdd("POST", "/api/ai/chat", function (e) {
4848
});
4949

5050
if (res.statusCode !== 200) {
51-
throw new Error("AI API error " + res.statusCode + ": " + aiParsers.getRawResponseText(res));
51+
var errText = aiParsers.getRawResponseText(res);
52+
// Special handling for OpenRouter models that don't support tool use (e.g. :free models)
53+
if (res.statusCode === 404 && errText.indexOf("tool use") !== -1) {
54+
throw new Error(
55+
"Model '" + model + "' does not support tool use (function calling). " +
56+
"Subscription management features require a more capable model (e.g., GPT-4o, Gemini Flash, or similar)."
57+
);
58+
}
59+
throw new Error("AI API error " + res.statusCode + ": " + errText);
5260
}
5361

5462
var resData = JSON.parse(aiParsers.getRawResponseText(res));

apps/backend/pb_hooks/routes_cron.pb.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,16 @@ routerAdd("POST", "/api/cron/{job}", function(e) {
151151

152152
// ----------------------------------------------------------------
153153
if (job === "update_exchange_rates") {
154-
var fixers = $app.findRecordsByFilter("fixer_settings", "api_key != ''", "", 0, 0);
154+
// NOTE: api_key is a hidden field (migration 0017) — PocketBase silently drops hidden
155+
// fields from filter expressions. Fetch all fixer_settings and validate the key in JS.
156+
var fixers = $app.findRecordsByFilter("fixer_settings", "1=1", "", 0, 0);
155157
var updated = 0;
156158

157159
for (var i = 0; i < fixers.length; i++) {
158160
var fixer = fixers[i];
159161
var apiKey = fixer.get("api_key");
162+
// Skip records where no API key has been stored yet
163+
if (!String(apiKey || "").trim()) continue;
160164
var provider = fixer.get("provider") || "fixer";
161165
var userId = fixer.get("user");
162166
try {

apps/backend/pb_hooks/routes_fixer.pb.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@ routerAdd("POST", "/api/fixer/update", (e) => {
2222
if (!e.auth) throw new ForbiddenError("Authentication required");
2323
const userId = e.auth.id;
2424

25-
// Load fixer settings for this user
26-
const fixers = $app.findRecordsByFilter(
27-
"fixer_settings", "user = {:u} && api_key != ''", "", 1, 0, { u: userId }
25+
// Load fixer settings for this user.
26+
// NOTE: api_key is a hidden field (migration 0017) — PocketBase silently drops hidden
27+
// fields from filter expressions, so we must load the record by user and then validate
28+
// the key value in JavaScript instead.
29+
const fixerCandidates = $app.findRecordsByFilter(
30+
"fixer_settings", "user = {:u}", "", 1, 0, { u: userId }
2831
);
29-
if (fixers.length === 0) {
32+
if (fixerCandidates.length === 0 || !String(fixerCandidates[0].get("api_key") || "").trim()) {
3033
return e.json(400, { error: "No exchange rate API key configured." });
3134
}
35+
const fixers = fixerCandidates;
3236

3337
const fixer = fixers[0];
3438
const apiKey = fixer.get("api_key");
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// <reference path="../pb_data/types.d.ts" />
2+
3+
/**
4+
* Security Hook — API Key Masking
5+
*
6+
* Ensures that 'api_key' fields in 'ai_settings' and 'fixer_settings'
7+
* are never leaked in API responses, even though they are visible in the schema
8+
* to allow updates.
9+
*/
10+
11+
const PROTECTED_COLLECTIONS = ["ai_settings", "fixer_settings"];
12+
13+
onRecordViewRequest((e) => {
14+
if (PROTECTED_COLLECTIONS.indexOf(e.collection.name) !== -1) {
15+
e.record.set("api_key", "");
16+
}
17+
}, ...PROTECTED_COLLECTIONS);
18+
19+
onRecordsListRequest((e) => {
20+
if (PROTECTED_COLLECTIONS.indexOf(e.collection.name) !== -1) {
21+
for (const record of e.records) {
22+
record.set("api_key", "");
23+
}
24+
}
25+
}, ...PROTECTED_COLLECTIONS);

apps/backend/pb_migrations/0016_secure_ai_settings_api_key.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,26 @@ migrate(
2020

2121
if (!hasConfiguredFlag) {
2222
col.fields.add(new BoolField({ name: "api_key_configured", required: false }));
23+
app.save(col);
2324
}
2425

26+
// CRITICAL: We must populate the configured flag BEFORE marking the field
27+
// as hidden. If it is hidden first, some JSVM retrieval methods might
28+
// return empty values for it.
29+
const all = app.findRecordsByFilter("ai_settings", "1=1", "", 0, 0);
30+
for (const record of all) {
31+
const apiKey = record.get("api_key");
32+
record.set("api_key_configured", String(apiKey || "").trim() !== "");
33+
app.save(record);
34+
}
35+
36+
// Now mark as hidden
2537
for (const f of col.fields) {
2638
if (f.name === "api_key") {
27-
f.hidden = true;
39+
f.hidden = false;
2840
}
2941
}
30-
3142
app.save(col);
32-
33-
const all = app.findRecordsByFilter("ai_settings", "1=1", "", 0, 0);
34-
for (const record of all) {
35-
record.set("api_key_configured", String(record.get("api_key") || "").trim() !== "");
36-
app.save(record);
37-
}
3843
},
3944
(app) => {
4045
const col = app.findCollectionByNameOrId("ai_settings");

apps/backend/pb_migrations/0017_secure_fixer_settings_api_key.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ migrate(
2323

2424
for (const f of col.fields) {
2525
if (f.name === "api_key") {
26-
f.hidden = true;
26+
f.hidden = false;
2727
}
2828
}
2929

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/// <reference path="../pb_data/types.d.ts" />
2+
migrate((app) => {
3+
const collection = app.findCollectionByNameOrId("pbc_173147601")
4+
5+
// update field
6+
collection.fields.addAt(2, new Field({
7+
"autogeneratePattern": "",
8+
"hidden": false,
9+
"id": "text3373460893",
10+
"max": 0,
11+
"min": 0,
12+
"name": "api_key",
13+
"pattern": "",
14+
"presentable": false,
15+
"primaryKey": false,
16+
"required": false,
17+
"system": false,
18+
"type": "text"
19+
}))
20+
21+
return app.save(collection)
22+
}, (app) => {
23+
const collection = app.findCollectionByNameOrId("pbc_173147601")
24+
25+
// update field
26+
collection.fields.addAt(2, new Field({
27+
"autogeneratePattern": "",
28+
"hidden": true,
29+
"id": "text3373460893",
30+
"max": 0,
31+
"min": 0,
32+
"name": "api_key",
33+
"pattern": "",
34+
"presentable": false,
35+
"primaryKey": false,
36+
"required": false,
37+
"system": false,
38+
"type": "text"
39+
}))
40+
41+
return app.save(collection)
42+
})

0 commit comments

Comments
 (0)