Skip to content

feat: show and sync complete Skills inventories - #349

Open
LceAn wants to merge 3 commits into
xiufengsun:mainfrom
LceAn:fix/skills-inventory-sync-i18n
Open

feat: show and sync complete Skills inventories#349
LceAn wants to merge 3 commits into
xiufengsun:mainfrom
LceAn:fix/skills-inventory-sync-i18n

Conversation

@LceAn

@LceAn LceAn commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • scan user-installed Skills from every supported local tool, including Codex, Claude, and ZCode plugin caches while excluding default built-in Skills
  • surface plugin-managed and other-device Skills as read-only inventory rows
  • publish a privacy-safe per-device metadata inventory and merge other-device results into the Skills page
  • make the Vite local Skills endpoint use the current checkout so desktop services on port 7680 cannot mask changes during development

Review updates

  • exclude built-in Skills at local discovery, upload projection, Edge sanitization/response, and cross-device merge boundaries
  • keep Skill names and metadata exactly as defined by each SKILL.md; the added locale dictionary changes have been removed
  • preserve local target state when the same Skill appears on another device
  • invalidate stale cloud inventory requests on auth/cloud-sync changes and bound cloud requests with a timeout
  • use stable id || directory identity for unmanaged Skill selection, list keys, and busy states

Privacy and safety

Only stable keys, display names, relative directories, tool IDs, scope flags, and device metadata are synced. SKILL.md content, descriptions, prompts, README URLs, target paths, and absolute paths are excluded. Both the browser projection and Edge function reject absolute/path-bearing identifiers. Remote and plugin-managed rows cannot be deleted, batch-selected, or retargeted.

Deployment required

  • apply migrations/20260721113000_add-device-skill-inventories.sql
  • deploy the tokentracker-account-skills Edge function from dashboard/edge-patches/tokentracker-account-skills.ts

Cross-device inventory respects the existing cloud-sync preference and degrades to the local inventory if auth, network, or backend sync is unavailable.

Validation

  • Dashboard Vitest: 76 files, 439 tests passed
  • Skills-focused Node tests: 16 passed
  • Dashboard typecheck, lint, and production build
  • validate:copy, validate:ui-hardcode, and validate:guardrails
  • git diff --check

Summary by CodeRabbit

  • New Features
    • Added cross-device Skills inventory storage and cloud publish/sync with per-device labels and “on devices” display.
    • Added plugin-cache discovery to surface read-only “inventory-only” skills, plus support for additional skill targets.
  • Bug Fixes
    • Excluded system-scoped skills and tightened handling so read-only/remote skills can’t be removed, toggled, selected, or bulk-synced.
    • Improved sanitization, JWT/auth verification, request size/time bounding, and ownership/revocation checks; fixed stale cloud overwrites.
    • Improved local “unused installed” filtering by excluding read-only skills.
  • Tests
    • Added guardrail coverage and expanded SkillsPage/UI and inventory merge tests.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds bounded local plugin inventory discovery, authenticated per-device cloud synchronization, sanitized cross-device merging, and dashboard handling for remote or read-only skills.

Changes

Skills inventory synchronization

Layer / File(s) Summary
Local inventory discovery
src/lib/skills-manager.js, src/lib/local-api.js, test/skills-manager-extras.test.js
Adds targets and bounded plugin-cache discovery, marks non-manageable targets, surfaces read-only inventory skills, and excludes read-only skills from unused-skill cleanup.
Cloud inventory contract
migrations/..., dashboard/edge-patches/..., dashboard/src/lib/skills-api.ts, dashboard/vite.config.js, test/skills-cloud-guardrails.test.js
Adds device inventory storage, authenticated GET/POST handling, sanitized persistence, timeout-bounded cloud requests, local routing, and guardrail tests.
Inventory metadata and merging
dashboard/src/lib/skills-inventory.ts, dashboard/src/lib/skills-inventory.test.ts, dashboard/src/pages/SkillsPage.jsx
Sanitizes publishable metadata, merges inventories by stable key, aggregates device sources, and enriches installed skills after local loading.
Read-only Skills UI
dashboard/src/pages/SkillsPage.jsx, dashboard/src/pages/SkillDetailPanel.jsx, dashboard/src/pages/SkillsPage.test.jsx, dashboard/src/content/i18n/*/core.json
Displays remote, plugin, source, and device metadata while preventing unsupported target, selection, synchronization, and removal actions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: mm7894215

Sequence Diagram(s)

sequenceDiagram
  participant SkillsPage
  participant skillsInventory
  participant skillsApi
  participant tokentrackerAccountSkills
  participant DeviceInventoryTable
  SkillsPage->>skillsInventory: buildSkillInventoryMetadata(localSkills)
  SkillsPage->>skillsApi: publishSkillInventory(accessToken, deviceId, skills)
  skillsApi->>tokentrackerAccountSkills: POST sanitized device inventory
  tokentrackerAccountSkills->>DeviceInventoryTable: upsert user/device skills
  SkillsPage->>skillsApi: getAccountSkillInventories(accessToken)
  skillsApi->>tokentrackerAccountSkills: GET account inventories
  tokentrackerAccountSkills->>DeviceInventoryTable: query active devices and inventories
  DeviceInventoryTable-->>tokentrackerAccountSkills: device inventory rows
  tokentrackerAccountSkills-->>skillsApi: devices payload
  skillsApi-->>SkillsPage: cloud inventories
  SkillsPage->>skillsInventory: mergeSkillInventories(localSkills, cloudPayload, deviceId)
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding complete Skills inventory display and cloud sync support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dashboard/src/pages/SkillsPage.jsx (1)

1132-1140: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Use stable identity skill.id || skill.directory for selection and busy states.

Unmanaged local skills lack an id and rely on skill.directory for identity. Using skill.id exclusively for Set membership causes all unmanaged skills to map to undefined. Consequently, selecting one unmanaged skill visually selects all of them, and executing a bulk action (like remove) will unintentionally delete all unmanaged skills. Additionally, busy spinners will cross-contaminate across unmanaged rows.

  • dashboard/src/pages/SkillsPage.jsx#L1132-L1140: Use const id = skill.id || skill.directory; and add/delete id from the next set.
  • dashboard/src/pages/SkillsPage.jsx#L188-L188: Change to busy={busyKey === targetBusyKey(skill.id || skill.directory, target.id)}.
  • dashboard/src/pages/SkillsPage.jsx#L557-L558: Change to checked={selectedIds.has(skill.id || skill.directory)}.
  • dashboard/src/pages/SkillsPage.jsx#L1107-L1109: Change to runMutation(targetBusyKey(skill.id || skill.directory, targetId)...).
  • dashboard/src/pages/SkillsPage.jsx#L1146-L1148: Change filter to selectedIds.has(s.id || s.directory).
  • dashboard/src/pages/SkillsPage.jsx#L1168-L1170: Change filter to selectedIds.has(s.id || s.directory).
  • dashboard/src/pages/SkillDetailPanel.jsx#L329-L329: Change to busy={busyKey === targetBusyKey(skill.id || skill.directory, target.id)}.
🤖 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 `@dashboard/src/pages/SkillsPage.jsx` around lines 1132 - 1140, Use the stable
identity skill.id || skill.directory throughout selection and busy-state
handling. In dashboard/src/pages/SkillsPage.jsx at lines 1132-1140, 188,
557-558, 1107-1109, 1146-1148, and 1168-1170, update handleToggleSelect, checked
state, targetBusyKey calls, and selectedIds filters to use that fallback
identity; make the corresponding busy-state update in
dashboard/src/pages/SkillDetailPanel.jsx at line 329.
🧹 Nitpick comments (1)
dashboard/src/lib/skills-api.ts (1)

131-136: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a request timeout for the cloud fetch.

fetchCloudSkillsJson has no AbortController/timeout, so a stalled cloud endpoint can leave publishSkillInventory/getAccountSkillInventories (and the Skills UI busy state) hanging indefinitely. A bounded timeout would degrade gracefully to local inventory, matching the fallback intent described in the PR.

🤖 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 `@dashboard/src/lib/skills-api.ts` around lines 131 - 136, Update
fetchCloudSkillsJson to use an AbortController with a bounded timeout when
issuing the cloud fetch, ensuring the timer is cleaned up after completion or
failure. Propagate timeout failures through the existing fallback handling so
publishSkillInventory and getAccountSkillInventories can degrade to local
inventory instead of remaining pending.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@dashboard/src/lib/skills-inventory.ts`:
- Around line 118-120: Update the existing-skill merge logic around
addDeviceSource so it preserves existing.targets exactly and does not union in
remote.targets; only append the device source while retaining remote-only target
state separately where applicable.

In `@dashboard/src/pages/SkillsPage.jsx`:
- Around line 836-854: The loadInstalled auth guard must invalidate in-flight
cloud inventory requests when authentication or cloud sync changes. Increment
cloudInventoryRequest before the guard, and when signedIn, cloud sync, deviceId,
or getAccessToken is unavailable, clear cloud-derived inventory rows and device
metadata before returning; preserve the existing requestId-based stale-response
check for valid cloud loads.

---

Outside diff comments:
In `@dashboard/src/pages/SkillsPage.jsx`:
- Around line 1132-1140: Use the stable identity skill.id || skill.directory
throughout selection and busy-state handling. In
dashboard/src/pages/SkillsPage.jsx at lines 1132-1140, 188, 557-558, 1107-1109,
1146-1148, and 1168-1170, update handleToggleSelect, checked state,
targetBusyKey calls, and selectedIds filters to use that fallback identity; make
the corresponding busy-state update in dashboard/src/pages/SkillDetailPanel.jsx
at line 329.

---

Nitpick comments:
In `@dashboard/src/lib/skills-api.ts`:
- Around line 131-136: Update fetchCloudSkillsJson to use an AbortController
with a bounded timeout when issuing the cloud fetch, ensuring the timer is
cleaned up after completion or failure. Propagate timeout failures through the
existing fallback handling so publishSkillInventory and
getAccountSkillInventories can degrade to local inventory instead of remaining
pending.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9bf0590e-fe14-40cb-91b2-19056e93ad26

📥 Commits

Reviewing files that changed from the base of the PR and between d3bb2e1 and 6d4c36d.

⛔ Files ignored due to path filters (1)
  • dashboard/src/content/copy.csv is excluded by !**/*.csv
📒 Files selected for processing (18)
  • dashboard/edge-patches/tokentracker-account-skills.ts
  • dashboard/src/content/i18n/de/core.json
  • dashboard/src/content/i18n/ja/core.json
  • dashboard/src/content/i18n/ko/core.json
  • dashboard/src/content/i18n/zh-TW/core.json
  • dashboard/src/content/i18n/zh/core.json
  • dashboard/src/lib/skills-api.ts
  • dashboard/src/lib/skills-inventory.test.ts
  • dashboard/src/lib/skills-inventory.ts
  • dashboard/src/pages/SkillDetailPanel.jsx
  • dashboard/src/pages/SkillsPage.jsx
  • dashboard/src/pages/SkillsPage.test.jsx
  • dashboard/vite.config.js
  • migrations/20260721113000_add-device-skill-inventories.sql
  • src/lib/local-api.js
  • src/lib/skills-manager.js
  • test/skills-cloud-guardrails.test.js
  • test/skills-manager-extras.test.js

Comment thread dashboard/src/lib/skills-inventory.ts Outdated
Comment thread dashboard/src/pages/SkillsPage.jsx
@xiufengsun

Copy link
Copy Markdown
Owner

Thanks for working on the complete Skills inventory and cross-device sync! Overall this expands Skill visibility nicely.

Before merging, we have a couple of optimization points to address:

  1. Filter out built-in Skills: Built-in skills (such as Codex default/built-in skills) are present by default on every machine. Showing them in the inventory adds unnecessary clutter. Please update the filtering logic to exclude built-in skills so only user-installed, plugin-managed, and custom cross-device skills are displayed.

  2. Remove skill localization: Skill names and metadata are defined directly inside each skill file (e.g. SKILL.md). They do not need to be translated or run through i18n dictionary files in dashboard/src/content/i18n/. Please keep skill names in their original defined form without extra copy/i18n translation mapping.

Please update the PR with these adjustments. Thank you!

@LceAn
LceAn force-pushed the fix/skills-inventory-sync-i18n branch from 6d4c36d to 16d32db Compare July 22, 2026 01:50
@LceAn

LceAn commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Updated in 16d32db based on your feedback:

  • Built-in Skills are now excluded from local discovery, uploads, Edge responses, and stale cross-device inventories.
  • The locale dictionary additions were removed; Skill names and metadata remain exactly as defined in each SKILL.md.
  • I also addressed the review findings around remote target-state contamination, stale auth requests, unmanaged Skill identity, and cloud request timeouts.

Rebased onto the latest main; the full Dashboard suite (76 files / 439 tests), focused Node tests, typecheck, lint, build, and project validators all pass.

@xiufengsun xiufengsun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the update. I re-reviewed the exact head (16d32db) on top of the latest main. The existing GitHub checks are green, but two merge blockers remain:

  1. The latest main now runs the locale coverage gate. This PR adds seven product UI strings under skills.inventory.* without locale entries, so npm run validate:locale fails for zh and zh-TW. Skill names and SKILL.md metadata should remain in their original form, but surrounding UI copy such as “Plugin”, “Other device”, and the read-only/device labels still needs localization. Please add translations for those seven UI keys without translating Skill-defined names or metadata.

  2. mergeSkillInventories() loses target information when the same Skill exists only on multiple remote devices with different targets. Once the first remote-only row is created, the existingIndex branch only appends the next device source and leaves the first device's targets/targetStates unchanged. For example, Claude on one remote device plus Codex on another renders only Claude while showing both device badges. Please preserve local targets for local matches, but merge target state for an existing remote-only row (or retain per-device target state), and add a regression test with different targets on two remote devices.

Validation on latest main + this PR otherwise looks good: 1,756/1,756 Node tests passed after building dashboard/dist, the Skills-focused Node and Dashboard tests passed, and typecheck, ESLint, production build, copy/UI/architecture guardrails all passed.

After these two issues are fixed, please also ensure the migration and tokentracker-account-skills Edge function are deployed before release; neither backend object is currently present in the linked InsForge project.

@LceAn
LceAn force-pushed the fix/skills-inventory-sync-i18n branch from 16d32db to 5bac50e Compare July 28, 2026 06:23
@LceAn

LceAn commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Updated in 5bac50e to address the latest review blockers:

  • Rebased the PR onto the current main (v0.87.1), resolving the previous merge conflict.
  • Added zh and zh-TW translations for the seven surrounding skills.inventory.* UI labels. Skill-defined names and SKILL.md metadata remain untouched.
  • Remote-only rows now merge targets and targetStates reported by different devices, while a matching local Skill keeps this machine's local targets and target states unchanged.
  • Added regression coverage for Claude on a remote Mac plus Codex on a remote Windows device, and strengthened the local-target preservation test.

The new head is mergeable and all GitHub checks pass: Linux test/validate/build, Windows build, macOS unit tests, CodeQL, and labeling. The migration and tokentracker-account-skills Edge function still need to be deployed by the maintainer before release, as noted in the review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@dashboard/edge-patches/tokentracker-account-skills.ts`:
- Around line 49-76: Update verifiedUserIdFromJwt to require payload.exp to be a
finite number and reject the token when exp is missing, invalid, or expired
before accepting payload.sub or payload.user_id. Preserve the existing
expiration comparison and identity fallback behavior for valid, unexpired
tokens.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 47b12566-4366-4fe1-91f6-182a754fec5b

📥 Commits

Reviewing files that changed from the base of the PR and between 16d32db and 5bac50e.

⛔ Files ignored due to path filters (1)
  • dashboard/src/content/copy.csv is excluded by !**/*.csv
📒 Files selected for processing (15)
  • dashboard/edge-patches/tokentracker-account-skills.ts
  • dashboard/src/content/i18n/zh-TW/core.json
  • dashboard/src/content/i18n/zh/core.json
  • dashboard/src/lib/skills-api.ts
  • dashboard/src/lib/skills-inventory.test.ts
  • dashboard/src/lib/skills-inventory.ts
  • dashboard/src/pages/SkillDetailPanel.jsx
  • dashboard/src/pages/SkillsPage.jsx
  • dashboard/src/pages/SkillsPage.test.jsx
  • dashboard/vite.config.js
  • migrations/20260721113000_add-device-skill-inventories.sql
  • src/lib/local-api.js
  • src/lib/skills-manager.js
  • test/skills-cloud-guardrails.test.js
  • test/skills-manager-extras.test.js
🚧 Files skipped from review as they are similar to previous changes (6)
  • migrations/20260721113000_add-device-skill-inventories.sql
  • src/lib/local-api.js
  • dashboard/src/pages/SkillsPage.test.jsx
  • dashboard/src/lib/skills-inventory.test.ts
  • dashboard/src/lib/skills-api.ts
  • dashboard/src/lib/skills-inventory.ts

Comment on lines +49 to +76
async function verifiedUserIdFromJwt(authHeader: string | null): Promise<string | null> {
if (!authHeader) return null;
const token = authHeader.replace(/^Bearer\s+/i, "").trim();
const secret = Deno.env.get("JWT_SECRET");
const parts = token.split(".");
if (!token || !secret || parts.length !== 3) return null;
try {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"],
);
const ok = await crypto.subtle.verify(
"HMAC",
key,
b64urlToBytes(parts[2]),
new TextEncoder().encode(`${parts[0]}.${parts[1]}`),
);
if (!ok) return null;
const payload = JSON.parse(new TextDecoder().decode(b64urlToBytes(parts[1]))) as Record<string, unknown>;
if (typeof payload.exp === "number" && Date.now() / 1000 > payload.exp) return null;
if (typeof payload.sub === "string" && payload.sub) return payload.sub;
if (typeof payload.user_id === "string" && payload.user_id) return payload.user_id;
} catch { /* invalid token */ }
return null;
}

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- Target file excerpt ---\n'
if [ -f dashboard/edge-patches/tokentracker-account-skills.ts ]; then
  nl -ba dashboard/edge-patches/tokentracker-account-skills.ts | sed -n '1,140p'
else
  printf 'Target file not found\n'
fi

printf '\n--- Repository JWT/validation references ---\n'
rg -n "(verifiedUserIdFromJwt|JWT_SECRET|access_token|payload\.exp|exp|aud|sub|user_id|auth\.users|insert\(\[)" -S --glob '!node_modules' --glob '!dist' --glob '!build' . | head -n 200

printf '\n--- Edge pricing parity test references if present ---\n'
if [ -f dashboard/test/edge-pricing-parity.test.js ]; then
  wc -l dashboard/test/edge-pricing-parity.test.js
  rg -n "tokentracker-account-skills|price|exp|JWT_SECRET|token" dashboard/test/edge-pricing-parity.test.js -S || true
fi

Repository: mm7894215/TokenTracker

Length of output: 251


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- Target file excerpt ---'
if [ -f dashboard/edge-patches/tokentracker-account-skills.ts ]; then
  nl -ba dashboard/edge-patches/tokentracker-account-skills.ts | sed -n '1,140p'
else
  printf '%s\n' 'Target file not found'
fi

printf '\n%s\n' '--- Repository JWT/validation references ---'
rg -n "verifiedUserIdFromJwt|JWT_SECRET|access_token|payload\.exp|\.exp|exp|aud|sub|user_id|auth\.users|insert\(\[" -S . | head -n 200 || true

printf '\n%s\n' '--- Edge pricing parity test references if present ---'
if [ -f dashboard/test/edge-pricing-parity.test.js ]; then
  wc -l dashboard/test/edge-pricing-parity.test.js
  rg -n "tokentracker-account-skills|price|exp|JWT_SECRET|token" dashboard/test/edge-pricing-parity.test.js || true
fi

Repository: mm7894215/TokenTracker

Length of output: 25474


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- Target file line count and relevant excerpt ---'
if [ -f dashboard/edge-patches/tokentracker-account-skills.ts ]; then
  wc -l dashboard/edge-patches/tokentracker-account-skills.ts
  sed -n '1,160p' dashboard/edge-patches/tokentracker-account-skills.ts | cat -n
else
  printf '%s\n' 'Target file not found'
fi

printf '\n%s\n' '--- Targeted JWT/validation matches ---'
grep -EIn "verifiedUserIdFromJwt|JWT_SECRET|payload\.exp|exp|sub|user_id|insert\\(\\[|onConflict" dashboard/edge-patches/tokentracker-account-skills.ts || true

printf '\n%s\n' '--- Edge pricing parity test file ---'
if [ -f dashboard/test/edge-pricing-parity.test.js ]; then
  wc -l dashboard/test/edge-pricing-parity.test.js
  sed -n '1,260p' dashboard/test/edge-pricing-parity.test.js | cat -n
else
  printf '%s\n' 'dashboard/test/edge-pricing-parity.test.js not found'
fi

printf '\n%s\n' '--- Edge patch directory files ---'
ls -1 dashboard/edge-patches/*.ts 2>/dev/null || true

Repository: mm7894215/TokenTracker

Length of output: 8644


Broken Authentication (CWE-613): Insufficient Session Expiration

Reachability: External
● Entry
  dashboard/src/pages/SkillsPage.jsx
│
▼
● Hop
  dashboard/src/lib/skills-inventory.ts:31
  cleanRelativePath
│
▼
● Sink
  dashboard/edge-patches/tokentracker-account-skills.ts

Require JWT exp validation instead accepting a token without it.

verifiedUserIdFromJwt only expires the JWT when payload.exp is a number; otherwise it returns on sub/user_id. A token without exp is therefore treated as never-expiring, so require exp to be present and valid before authorizing the endpoint.

🔒 Proposed fix
-    if (typeof payload.exp === "number" && Date.now() / 1000 > payload.exp) return null;
+    if (typeof payload.exp !== "number" || Date.now() / 1000 > payload.exp) return null;

Whether this is defense-in-depth or an exploit gap depends on whether InsForge-issued JWTs are guaranteed to include exp; if not, reject tokens missing it before returning the user identity.

📝 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
async function verifiedUserIdFromJwt(authHeader: string | null): Promise<string | null> {
if (!authHeader) return null;
const token = authHeader.replace(/^Bearer\s+/i, "").trim();
const secret = Deno.env.get("JWT_SECRET");
const parts = token.split(".");
if (!token || !secret || parts.length !== 3) return null;
try {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"],
);
const ok = await crypto.subtle.verify(
"HMAC",
key,
b64urlToBytes(parts[2]),
new TextEncoder().encode(`${parts[0]}.${parts[1]}`),
);
if (!ok) return null;
const payload = JSON.parse(new TextDecoder().decode(b64urlToBytes(parts[1]))) as Record<string, unknown>;
if (typeof payload.exp === "number" && Date.now() / 1000 > payload.exp) return null;
if (typeof payload.sub === "string" && payload.sub) return payload.sub;
if (typeof payload.user_id === "string" && payload.user_id) return payload.user_id;
} catch { /* invalid token */ }
return null;
}
async function verifiedUserIdFromJwt(authHeader: string | null): Promise<string | null> {
if (!authHeader) return null;
const token = authHeader.replace(/^Bearer\s+/i, "").trim();
const secret = Deno.env.get("JWT_SECRET");
const parts = token.split(".");
if (!token || !secret || parts.length !== 3) return null;
try {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"],
);
const ok = await crypto.subtle.verify(
"HMAC",
key,
b64urlToBytes(parts[2]),
new TextEncoder().encode(`${parts[0]}.${parts[1]}`),
);
if (!ok) return null;
const payload = JSON.parse(new TextDecoder().decode(b64urlToBytes(parts[1]))) as Record<string, unknown>;
if (typeof payload.exp !== "number" || Date.now() / 1000 > payload.exp) return null;
if (typeof payload.sub === "string" && payload.sub) return payload.sub;
if (typeof payload.user_id === "string" && payload.user_id) return payload.user_id;
} catch { /* invalid token */ }
return null;
}
🤖 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 `@dashboard/edge-patches/tokentracker-account-skills.ts` around lines 49 - 76,
Update verifiedUserIdFromJwt to require payload.exp to be a finite number and
reject the token when exp is missing, invalid, or expired before accepting
payload.sub or payload.user_id. Preserve the existing expiration comparison and
identity fallback behavior for valid, unexpired tokens.

@xiufengsun

Copy link
Copy Markdown
Owner

Thanks for the updates — the privacy boundary and read-only handling look solid now. Two things to fix before merge:

  1. Browse tab blocks local install for skills that only exist elsewhere. Remote-only rows and read-only plugin-cache rows feed into installedKeys, so the Browse card flips to "Manage" and there's no way to install the skill on this machine — which is the main point of cross-device visibility. Please skip remote / inventoryOnly / readOnly rows when building installedKeys (the dir:<leaf> fallback key from plugin rows has the same problem with same-named skills).

  2. Promise.all([get, publish]) couples the two requests. If publish fails (e.g. this device was revoked → 404), the GET result is discarded too and the user loses the other-device view entirely. Promise.allSettled and merging whenever the GET succeeds would keep it degradable.

Minor, fine as follow-ups: inventory rows for revoked devices are never cleaned up, and every mutation triggers a full GET+POST round trip (a projection-hash skip would help).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants