mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
fix(recipes): align the X secret name with the resolver — X_API_BEARER_TOKEN (#2789 defect 2) (#3649)
Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. The docs and recipe pinned `X_BEARER_TOKEN` while the resolver only ever read `X_API_BEARER_TOKEN` — so no single name worked and the integration could not be configured by following its own documentation. Renamed the dead documented side; reverting fails exactly 2 of the 3 new tests. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN on the current base after batches 1 and 2 landed, not a stale one. Known gap, recorded rather than hidden: no live X API call was made.
This commit is contained in:
+15
-7
@@ -1,12 +1,12 @@
|
||||
---
|
||||
id: x-to-brain
|
||||
name: X-to-Brain
|
||||
version: 0.8.2
|
||||
version: 0.8.3
|
||||
description: Twitter timeline, mentions, and keyword monitoring flow into brain pages. Tracks deletions, engagement velocity, OCR on images, and real-time alerts.
|
||||
category: sense
|
||||
requires: []
|
||||
secrets:
|
||||
- name: X_BEARER_TOKEN
|
||||
- name: X_API_BEARER_TOKEN
|
||||
description: X API v2 Bearer token (Basic tier minimum, $200/mo for full archive search)
|
||||
where: https://developer.x.com/en/portal/dashboard — create a project + app, copy the Bearer Token from "Keys and tokens"
|
||||
- name: X_HANDLE
|
||||
@@ -16,7 +16,7 @@ health_checks:
|
||||
- type: http
|
||||
url: "https://api.x.com/2/users/by/username/$X_HANDLE"
|
||||
auth: bearer
|
||||
auth_token: "$X_BEARER_TOKEN"
|
||||
auth_token: "$X_API_BEARER_TOKEN"
|
||||
label: "X API"
|
||||
setup_time: 15 min
|
||||
cost_estimate: "$0-200/mo (Free tier: 1 app, read-only. Basic: $200/mo for search + higher limits)"
|
||||
@@ -118,11 +118,11 @@ Tell the user:
|
||||
Note: Free tier gives read-only access with low limits. Basic tier ($200/mo)
|
||||
gives search/recent endpoint and higher limits. Pro tier gets full archive search."
|
||||
|
||||
Set both `X_BEARER_TOKEN` and `X_HANDLE` in the environment. Validate immediately
|
||||
Set both `X_API_BEARER_TOKEN` and `X_HANDLE` in the environment. Validate immediately
|
||||
(app-only bearer tokens cannot call `/users/me` — that endpoint requires
|
||||
user-context OAuth — so validation uses the by-username lookup):
|
||||
```bash
|
||||
curl -sf -H "Authorization: Bearer $X_BEARER_TOKEN" \
|
||||
curl -sf -H "Authorization: Bearer $X_API_BEARER_TOKEN" \
|
||||
"https://api.x.com/2/users/by/username/$X_HANDLE" \
|
||||
&& echo "PASS: X API connected" \
|
||||
|| echo "FAIL: X API token invalid"
|
||||
@@ -138,7 +138,7 @@ starting with 'AAA...', (3) if you just created the app, the token is valid imme
|
||||
|
||||
```bash
|
||||
# Look up the user's X user ID from their handle
|
||||
curl -sf -H "Authorization: Bearer $X_BEARER_TOKEN" \
|
||||
curl -sf -H "Authorization: Bearer $X_API_BEARER_TOKEN" \
|
||||
"https://api.x.com/2/users/by/username/$X_HANDLE" | grep -o '"id":"[^"]*"'
|
||||
```
|
||||
|
||||
@@ -210,7 +210,7 @@ The agent should review collected data 2-3x daily and run enrichment.
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.gbrain/integrations/x-to-brain
|
||||
echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","event":"setup_complete","source_version":"0.8.2","status":"ok","details":{"user_id":"X_USER_ID"}}' >> ~/.gbrain/integrations/x-to-brain/heartbeat.jsonl
|
||||
echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","event":"setup_complete","source_version":"0.8.3","status":"ok","details":{"user_id":"X_USER_ID"}}' >> ~/.gbrain/integrations/x-to-brain/heartbeat.jsonl
|
||||
```
|
||||
|
||||
## Production Patterns (v0.8.1)
|
||||
@@ -438,6 +438,14 @@ Free tier works for personal monitoring. Basic tier needed for keyword search.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Upgrading from recipe v0.8.2 or earlier (token shows [missing] after upgrade):**
|
||||
- Older versions of this recipe named the token `X_BEARER_TOKEN`. The canonical
|
||||
name is `X_API_BEARER_TOKEN` — the name the built-in `x_handle_to_tweet`
|
||||
resolver reads. Rename the variable wherever you set it (shell profile, cron
|
||||
environment, `.env`) — same value, new name. A collector installed under the
|
||||
old name keeps running either way; the rename is what makes the integrations
|
||||
dashboard and the resolver see the token.
|
||||
|
||||
**API returns 403:**
|
||||
- Check your app has the right access level (Read or Read+Write)
|
||||
- Free tier apps can only use basic endpoints
|
||||
|
||||
@@ -42,7 +42,7 @@ interface FeatureScanResult {
|
||||
const RECIPE_META = [
|
||||
{ id: 'email-to-brain', name: 'Email to Brain', secrets: ['GMAIL_APP_PASSWORD'] },
|
||||
{ id: 'calendar-to-brain', name: 'Calendar Sync', secrets: ['GOOGLE_CALENDAR_API_KEY'] },
|
||||
{ id: 'x-to-brain', name: 'X/Twitter to Brain', secrets: ['X_BEARER_TOKEN'] },
|
||||
{ id: 'x-to-brain', name: 'X/Twitter to Brain', secrets: ['X_API_BEARER_TOKEN'] },
|
||||
{ id: 'twilio-voice-brain', name: 'Voice to Brain', secrets: ['TWILIO_AUTH_TOKEN'] },
|
||||
{ id: 'meeting-sync', name: 'Meeting Sync', secrets: ['CIRCLEBACK_API_KEY'] },
|
||||
{ id: 'credential-gateway', name: 'Credential Gateway', secrets: ['OAUTH_CLIENT_SECRET'] },
|
||||
|
||||
@@ -23,6 +23,52 @@ describe('recipe metadata', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// #2789: the x-to-brain secret name must be the one the resolver actually
|
||||
// reads. The recipe + RECIPE_META used to pin X_BEARER_TOKEN while the
|
||||
// x_handle_to_tweet resolver reads only config x_api_bearer_token / env
|
||||
// X_API_BEARER_TOKEN — so no single name worked end-to-end. All three
|
||||
// surfaces must agree on the resolver's canonical name.
|
||||
describe('x-to-brain secret name alignment (#2789)', () => {
|
||||
const read = (p: string) => {
|
||||
const { readFileSync } = require('fs');
|
||||
return readFileSync(new URL(p, import.meta.url), 'utf-8') as string;
|
||||
};
|
||||
|
||||
it('features registry pins the resolver-canonical name', () => {
|
||||
const src = read('../src/commands/features.ts');
|
||||
expect(src).toContain("{ id: 'x-to-brain', name: 'X/Twitter to Brain', secrets: ['X_API_BEARER_TOKEN'] }");
|
||||
});
|
||||
|
||||
it('the x-to-brain recipe declares and uses only the canonical name', async () => {
|
||||
const { parseRecipe } = await import('../src/commands/integrations.ts');
|
||||
const raw = read('../recipes/x-to-brain.md');
|
||||
const recipe = parseRecipe(raw, 'x-to-brain.md');
|
||||
expect(recipe).not.toBeNull();
|
||||
// Frontmatter: the declared secret is the canonical name.
|
||||
const secretNames = recipe!.frontmatter.secrets.map(s => s.name);
|
||||
expect(secretNames).toContain('X_API_BEARER_TOKEN');
|
||||
expect(secretNames).not.toContain('X_BEARER_TOKEN');
|
||||
// Health check: the bearer interpolation uses the canonical name.
|
||||
const hc = recipe!.frontmatter.health_checks[0] as { auth_token?: string };
|
||||
expect(hc.auth_token).toBe('$X_API_BEARER_TOKEN');
|
||||
// Body: every $-interpolated token reference (curl examples etc.) is the
|
||||
// canonical name — catches a third misspelled variant, not just the exact
|
||||
// legacy string. (The legacy name may still appear as PROSE in the
|
||||
// upgrade/migration note; only $VAR references are load-bearing.)
|
||||
const tokenRefs = raw.match(/\$X_[A-Z_]*TOKEN\b/g) ?? [];
|
||||
expect(tokenRefs.length).toBeGreaterThan(0);
|
||||
for (const ref of tokenRefs) expect(ref).toBe('$X_API_BEARER_TOKEN');
|
||||
});
|
||||
|
||||
it('the resolver reads the same env var the recipe documents', () => {
|
||||
// Alignment guard (not a behavior test — resolver behavior is pinned in
|
||||
// test/resolvers.test.ts): if the resolver's env name ever changes, this
|
||||
// forces the recipe + registry to move with it.
|
||||
const resolver = read('../src/core/resolvers/builtin/x-api/handle-to-tweet.ts');
|
||||
expect(resolver).toContain('process.env.X_API_BEARER_TOKEN');
|
||||
});
|
||||
});
|
||||
|
||||
// Test brain_score in BrainHealth type
|
||||
describe('BrainHealth type', () => {
|
||||
it('includes brain_score field', async () => {
|
||||
|
||||
Reference in New Issue
Block a user