mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-27 21:05:34 +00:00
fix(frontend): send local API key as Bearer on /v1 + /api requests (#471)
When `jarvis serve` runs with an API key configured, AuthMiddleware 401s every /v1 and /api request that lacks a Bearer token. The frontend never sent one, so telemetry, managed-agents, savings, etc. all failed (#266). - Add getApiKey() (reads settings.apiKey, with optional VITE_OPENJARVIS_API_KEY build-time override) and authHeaders() to api.ts, plus an apiFetch() wrapper that prepends getBase() and injects the Bearer header on every local-server call. Route all /v1 + /api fetches through it so none can omit auth. - Add `apiKey` to the Settings model (store.ts) and a password field in Settings → Connection so users can enter it. Keyless local servers are unaffected: with no key, no Authorization header is sent (byte-for-byte unchanged). The Supabase savings path keeps its own anon key — not conflated with the local key. Bootstraps vitest (no prior frontend test runner) + a `test` script, and adds api.auth.test.ts covering getApiKey/authHeaders. Verified: tsc --noEmit clean, vitest 6/6, vite build succeeds. Deferred (not in scope): WebSocket auth (browsers can't set WS headers) and Tauri auto-injecting a generated key. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
690b95e050
commit
4b4fd587b2
Generated
+441
-1
@@ -48,7 +48,8 @@
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "~5.7.0",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-pwa": "^1.2.0"
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vitest": "^3.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -4064,6 +4065,17 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
@@ -4136,6 +4148,13 @@
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -4274,6 +4293,131 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz",
|
||||
"integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "3.2.6",
|
||||
"@vitest/utils": "3.2.6",
|
||||
"chai": "^5.2.0",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz",
|
||||
"integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.2.6",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz",
|
||||
"integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz",
|
||||
"integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.2.6",
|
||||
"pathe": "^2.0.3",
|
||||
"strip-literal": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz",
|
||||
"integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.6",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz",
|
||||
"integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyspy": "^4.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz",
|
||||
"integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.6",
|
||||
"loupe": "^3.1.4",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
@@ -4414,6 +4558,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-types": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz",
|
||||
@@ -4654,6 +4808,16 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||
@@ -4741,6 +4905,23 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
|
||||
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assertion-error": "^2.0.1",
|
||||
"check-error": "^2.1.1",
|
||||
"deep-eql": "^5.0.1",
|
||||
"loupe": "^3.1.0",
|
||||
"pathval": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
@@ -4793,6 +4974,16 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
|
||||
"integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
@@ -5390,6 +5581,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
@@ -5749,6 +5950,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
@@ -5975,6 +6183,16 @@
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
@@ -8156,6 +8374,13 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
||||
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lowlight": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
|
||||
@@ -9742,6 +9967,23 @@
|
||||
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathval": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
|
||||
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -10987,6 +11229,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
@@ -11072,6 +11321,13 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
@@ -11081,6 +11337,13 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stdin-discarder": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
||||
@@ -11294,6 +11557,26 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
|
||||
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^9.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal/node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/style-to-js": {
|
||||
"version": "1.1.21",
|
||||
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
|
||||
@@ -11459,6 +11742,20 @@
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@@ -11475,6 +11772,36 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
|
||||
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
|
||||
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyspy": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
|
||||
"integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.0.27",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz",
|
||||
@@ -12164,6 +12491,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
|
||||
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"debug": "^4.4.1",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"pathe": "^2.0.3",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"bin": {
|
||||
"vite-node": "vite-node.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-pwa": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz",
|
||||
@@ -12195,6 +12545,79 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz",
|
||||
"integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.6",
|
||||
"@vitest/mocker": "3.2.6",
|
||||
"@vitest/pretty-format": "^3.2.6",
|
||||
"@vitest/runner": "3.2.6",
|
||||
"@vitest/snapshot": "3.2.6",
|
||||
"@vitest/spy": "3.2.6",
|
||||
"@vitest/utils": "3.2.6",
|
||||
"chai": "^5.2.0",
|
||||
"debug": "^4.4.1",
|
||||
"expect-type": "^1.2.1",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.2",
|
||||
"std-env": "^3.9.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^0.3.2",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"tinypool": "^1.1.1",
|
||||
"tinyrainbow": "^2.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
|
||||
"vite-node": "3.2.4",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.2.6",
|
||||
"@vitest/ui": "3.2.6",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/debug": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/web-namespaces": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
|
||||
@@ -12343,6 +12766,23 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/workbox-background-sync": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz",
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"build": "tsc -b && vite build",
|
||||
"build:tauri": "tsc -b && vite build --outDir dist",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
"tauri": "tauri",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
@@ -54,6 +55,7 @@
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "~5.7.0",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-pwa": "^1.2.0"
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vitest": "^3.2.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
// Regression for #266: the frontend must send the local API key as a Bearer
|
||||
// token on /v1 + /api requests, or `jarvis serve` with a key configured 401s
|
||||
// every data-plane call. These tests cover the pure helpers (getApiKey,
|
||||
// authHeaders) that source the key and build the header.
|
||||
|
||||
const SETTINGS_KEY = 'openjarvis-settings';
|
||||
|
||||
// Minimal in-memory localStorage stub so the helpers can run under node
|
||||
// (no jsdom dependency).
|
||||
class MemoryStorage {
|
||||
private store = new Map<string, string>();
|
||||
getItem(k: string): string | null {
|
||||
return this.store.has(k) ? (this.store.get(k) as string) : null;
|
||||
}
|
||||
setItem(k: string, v: string): void {
|
||||
this.store.set(k, String(v));
|
||||
}
|
||||
removeItem(k: string): void {
|
||||
this.store.delete(k);
|
||||
}
|
||||
clear(): void {
|
||||
this.store.clear();
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(globalThis as unknown as { localStorage: MemoryStorage }).localStorage =
|
||||
new MemoryStorage();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(globalThis as unknown as { localStorage?: MemoryStorage }).localStorage =
|
||||
undefined;
|
||||
});
|
||||
|
||||
async function freshApi() {
|
||||
// Re-import to pick up the current localStorage stub.
|
||||
return await import('./api');
|
||||
}
|
||||
|
||||
describe('getApiKey', () => {
|
||||
it('returns empty string when no key is configured', async () => {
|
||||
const { getApiKey } = await freshApi();
|
||||
expect(getApiKey()).toBe('');
|
||||
});
|
||||
|
||||
it('reads apiKey from the openjarvis-settings localStorage blob', async () => {
|
||||
localStorage.setItem(
|
||||
SETTINGS_KEY,
|
||||
JSON.stringify({ apiUrl: 'http://x', apiKey: 'sk-local-123' }),
|
||||
);
|
||||
const { getApiKey } = await freshApi();
|
||||
expect(getApiKey()).toBe('sk-local-123');
|
||||
});
|
||||
|
||||
it('returns empty string when the blob has no apiKey field', async () => {
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ apiUrl: 'http://x' }));
|
||||
const { getApiKey } = await freshApi();
|
||||
expect(getApiKey()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('authHeaders', () => {
|
||||
it('omits Authorization when no key is set (keyless default unchanged)', async () => {
|
||||
const { authHeaders } = await freshApi();
|
||||
expect(authHeaders()).toEqual({});
|
||||
});
|
||||
|
||||
it('adds a Bearer Authorization header when a key is set', async () => {
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ apiKey: 'sk-local-123' }));
|
||||
const { authHeaders } = await freshApi();
|
||||
expect(authHeaders()).toEqual({ Authorization: 'Bearer sk-local-123' });
|
||||
});
|
||||
|
||||
it('merges extra headers alongside Authorization', async () => {
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ apiKey: 'sk-local-123' }));
|
||||
const { authHeaders } = await freshApi();
|
||||
expect(authHeaders({ 'Content-Type': 'application/json' })).toEqual({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer sk-local-123',
|
||||
});
|
||||
});
|
||||
});
|
||||
+91
-47
@@ -52,6 +52,50 @@ export const getBase = (): string => {
|
||||
return '';
|
||||
};
|
||||
|
||||
// Resolve the local server API key (OPENJARVIS_API_KEY). When `jarvis serve`
|
||||
// is started with a key, AuthMiddleware 401s every /v1 and /api request that
|
||||
// lacks a Bearer token — so the frontend must send it (#266). Sourced from the
|
||||
// same settings blob as the API URL, with an optional build-time env override.
|
||||
// Returns '' when unset, so a keyless local server keeps working unchanged.
|
||||
export const getApiKey = (): string => {
|
||||
try {
|
||||
const raw = localStorage.getItem('openjarvis-settings');
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed.apiKey) return String(parsed.apiKey);
|
||||
}
|
||||
} catch {}
|
||||
if (import.meta.env.VITE_OPENJARVIS_API_KEY) {
|
||||
return import.meta.env.VITE_OPENJARVIS_API_KEY as string;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// Build request headers with the Bearer Authorization token when a local key
|
||||
// is configured, merging any caller-supplied headers. Adds no Authorization
|
||||
// header when no key is set, so keyless local dev is byte-for-byte unchanged.
|
||||
export const authHeaders = (
|
||||
extra: Record<string, string> = {},
|
||||
): Record<string, string> => {
|
||||
const key = getApiKey();
|
||||
return key ? { ...extra, Authorization: `Bearer ${key}` } : { ...extra };
|
||||
};
|
||||
|
||||
// Centralized fetch for the local server: prepends getBase() and injects the
|
||||
// Bearer auth header (when a key is set) on every call. Using this everywhere
|
||||
// guarantees no /v1 or /api request is sent without auth — the bug in #266 was
|
||||
// that direct fetch() calls omitted the header and 401'd. `path` is the
|
||||
// server-relative path (e.g. "/v1/savings").
|
||||
export const apiFetch = (
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
): Promise<Response> => {
|
||||
const headers = authHeaders(
|
||||
(init.headers as Record<string, string> | undefined) ?? {},
|
||||
);
|
||||
return fetch(`${getBase()}${path}`, { ...init, headers });
|
||||
};
|
||||
|
||||
async function tauriInvoke<T>(command: string, args: Record<string, unknown> = {}): Promise<T> {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
const apiUrl = getBase();
|
||||
@@ -95,14 +139,14 @@ export async function fetchModels(): Promise<ModelInfo[]> {
|
||||
// Fall through to fetch
|
||||
}
|
||||
}
|
||||
const res = await fetch(`${getBase()}/v1/models`);
|
||||
const res = await apiFetch(`/v1/models`);
|
||||
if (!res.ok) throw new Error(`Failed to fetch models: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.data || [];
|
||||
}
|
||||
|
||||
export async function fetchRecommendedModel(): Promise<{ model: string; reason: string }> {
|
||||
const res = await fetch(`${getBase()}/v1/recommended-model`);
|
||||
const res = await apiFetch(`/v1/recommended-model`);
|
||||
if (!res.ok) return { model: '', reason: 'Failed to fetch' };
|
||||
return res.json();
|
||||
}
|
||||
@@ -119,7 +163,7 @@ export async function pullModel(modelName: string): Promise<void> {
|
||||
throw new Error(e?.message || e || 'Download failed');
|
||||
}
|
||||
}
|
||||
const res = await fetch(`${getBase()}/v1/models/pull`, {
|
||||
const res = await apiFetch(`/v1/models/pull`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model: modelName }),
|
||||
@@ -140,7 +184,7 @@ export async function deleteModel(modelName: string): Promise<void> {
|
||||
throw new Error(e?.message || e || 'Delete failed');
|
||||
}
|
||||
}
|
||||
const res = await fetch(`${getBase()}/v1/models/${encodeURIComponent(modelName)}`, {
|
||||
const res = await apiFetch(`/v1/models/${encodeURIComponent(modelName)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -173,13 +217,13 @@ export async function preloadModel(modelName: string): Promise<void> {
|
||||
}
|
||||
|
||||
export async function fetchSavings(): Promise<SavingsData> {
|
||||
const res = await fetch(`${getBase()}/v1/savings`);
|
||||
const res = await apiFetch(`/v1/savings`);
|
||||
if (!res.ok) throw new Error(`Failed to fetch savings: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchServerInfo(): Promise<ServerInfo> {
|
||||
const res = await fetch(`${getBase()}/v1/info`);
|
||||
const res = await apiFetch(`/v1/info`);
|
||||
if (!res.ok) throw new Error(`Failed to fetch server info: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
@@ -221,7 +265,7 @@ export async function fetchEnergy(): Promise<unknown> {
|
||||
return await tauriInvoke('fetch_energy', { apiUrl: getBase() });
|
||||
} catch {}
|
||||
}
|
||||
const res = await fetch(`${getBase()}/v1/telemetry/energy`);
|
||||
const res = await apiFetch(`/v1/telemetry/energy`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
@@ -232,7 +276,7 @@ export async function fetchTelemetry(): Promise<unknown> {
|
||||
return await tauriInvoke('fetch_telemetry', { apiUrl: getBase() });
|
||||
} catch {}
|
||||
}
|
||||
const res = await fetch(`${getBase()}/v1/telemetry/stats`);
|
||||
const res = await apiFetch(`/v1/telemetry/stats`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
@@ -243,7 +287,7 @@ export async function fetchTraces(limit: number = 50): Promise<unknown> {
|
||||
return await tauriInvoke('fetch_traces', { apiUrl: getBase(), limit });
|
||||
} catch {}
|
||||
}
|
||||
const res = await fetch(`${getBase()}/v1/traces?limit=${limit}`);
|
||||
const res = await apiFetch(`/v1/traces?limit=${limit}`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
@@ -279,7 +323,7 @@ export async function transcribeAudio(audioBlob: Blob, filename = 'recording.web
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', audioBlob, filename);
|
||||
const res = await fetch(`${getBase()}/v1/speech/transcribe`, {
|
||||
const res = await apiFetch(`/v1/speech/transcribe`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
@@ -295,7 +339,7 @@ export async function fetchSpeechHealth(): Promise<SpeechHealth> {
|
||||
return { available: false };
|
||||
}
|
||||
}
|
||||
const res = await fetch(`${getBase()}/v1/speech/health`);
|
||||
const res = await apiFetch(`/v1/speech/health`);
|
||||
if (!res.ok) return { available: false };
|
||||
return res.json();
|
||||
}
|
||||
@@ -379,14 +423,14 @@ export interface AgentMessage {
|
||||
}
|
||||
|
||||
export async function fetchManagedAgents(): Promise<ManagedAgent[]> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents`);
|
||||
const res = await apiFetch(`/v1/managed-agents`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.agents || [];
|
||||
}
|
||||
|
||||
export async function fetchManagedAgent(agentId: string): Promise<ManagedAgent> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}`);
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
@@ -397,7 +441,7 @@ export async function createManagedAgent(body: {
|
||||
template_id?: string;
|
||||
config?: Record<string, unknown>;
|
||||
}): Promise<ManagedAgent> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents`, {
|
||||
const res = await apiFetch(`/v1/managed-agents`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
@@ -410,7 +454,7 @@ export async function updateManagedAgent(
|
||||
agentId: string,
|
||||
body: Partial<{ name: string; agent_type: string; config: Record<string, unknown> }>,
|
||||
): Promise<ManagedAgent> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}`, {
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
@@ -420,29 +464,29 @@ export async function updateManagedAgent(
|
||||
}
|
||||
|
||||
export async function deleteManagedAgent(agentId: string): Promise<void> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}`, { method: 'DELETE' });
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
}
|
||||
|
||||
export async function pauseManagedAgent(agentId: string): Promise<void> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/pause`, { method: 'POST' });
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/pause`, { method: 'POST' });
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
}
|
||||
|
||||
export async function resumeManagedAgent(agentId: string): Promise<void> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/resume`, { method: 'POST' });
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/resume`, { method: 'POST' });
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
}
|
||||
|
||||
export async function fetchAgentTasks(agentId: string): Promise<AgentTask[]> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/tasks`);
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/tasks`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.tasks || [];
|
||||
}
|
||||
|
||||
export async function createAgentTask(agentId: string, description: string): Promise<AgentTask> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/tasks`, {
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ description }),
|
||||
@@ -452,7 +496,7 @@ export async function createAgentTask(agentId: string, description: string): Pro
|
||||
}
|
||||
|
||||
export async function fetchAgentChannels(agentId: string): Promise<ChannelBinding[]> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/channels`);
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/channels`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.bindings || [];
|
||||
@@ -496,7 +540,7 @@ export async function sendblueVerify(
|
||||
apiKeyId: string,
|
||||
apiSecretKey: string,
|
||||
): Promise<{ valid: boolean; numbers: string[]; raw: unknown }> {
|
||||
const res = await fetch(`${getBase()}/v1/channels/sendblue/verify`, {
|
||||
const res = await apiFetch(`/v1/channels/sendblue/verify`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ api_key_id: apiKeyId, api_secret_key: apiSecretKey }),
|
||||
@@ -513,7 +557,7 @@ export async function sendblueRegisterWebhook(
|
||||
apiSecretKey: string,
|
||||
webhookUrl: string,
|
||||
): Promise<{ registered: boolean; status: number }> {
|
||||
const res = await fetch(`${getBase()}/v1/channels/sendblue/register-webhook`, {
|
||||
const res = await apiFetch(`/v1/channels/sendblue/register-webhook`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -535,7 +579,7 @@ export async function sendblueTest(
|
||||
fromNumber: string,
|
||||
toNumber: string,
|
||||
): Promise<{ sent: boolean; status: number }> {
|
||||
const res = await fetch(`${getBase()}/v1/channels/sendblue/test`, {
|
||||
const res = await apiFetch(`/v1/channels/sendblue/test`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -553,20 +597,20 @@ export async function sendblueTest(
|
||||
}
|
||||
|
||||
export async function sendblueHealth(): Promise<{ channel_connected: boolean; bridge_wired: boolean; ready: boolean }> {
|
||||
const res = await fetch(`${getBase()}/v1/channels/sendblue/health`);
|
||||
const res = await apiFetch(`/v1/channels/sendblue/health`);
|
||||
if (!res.ok) return { channel_connected: false, bridge_wired: false, ready: false };
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchTemplates(): Promise<AgentTemplate[]> {
|
||||
const res = await fetch(`${getBase()}/v1/templates`);
|
||||
const res = await apiFetch(`/v1/templates`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.templates || [];
|
||||
}
|
||||
|
||||
export async function runManagedAgent(agentId: string): Promise<void> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/run`, { method: 'POST' });
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/run`, { method: 'POST' });
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({ detail: res.statusText }));
|
||||
throw new Error(body.detail || `Failed: ${res.status}`);
|
||||
@@ -574,7 +618,7 @@ export async function runManagedAgent(agentId: string): Promise<void> {
|
||||
}
|
||||
|
||||
export async function recoverManagedAgent(agentId: string): Promise<{ recovered: boolean; checkpoint: unknown }> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/recover`, { method: 'POST' });
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/recover`, { method: 'POST' });
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({ detail: res.statusText }));
|
||||
throw new Error(body.detail || `Failed: ${res.status}`);
|
||||
@@ -589,7 +633,7 @@ export async function fetchAgentState(agentId: string): Promise<{
|
||||
messages: AgentMessage[];
|
||||
checkpoint: unknown;
|
||||
}> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/state`);
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/state`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
@@ -618,7 +662,7 @@ export async function sendAgentMessage(
|
||||
onDone?: (fullContent: string, usage?: Record<string, number>, telemetry?: Record<string, unknown>) => void;
|
||||
},
|
||||
): Promise<AgentMessage> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/messages`, {
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/messages`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content, mode, stream: true }),
|
||||
@@ -733,7 +777,7 @@ export async function sendAgentMessage(
|
||||
* the `/v1/agents/events` WebSocket and the resulting trace.
|
||||
*/
|
||||
export async function askAgent(agentId: string, content: string): Promise<AgentMessage> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/messages`, {
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/messages`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content, mode: 'immediate', stream: false }),
|
||||
@@ -743,14 +787,14 @@ export async function askAgent(agentId: string, content: string): Promise<AgentM
|
||||
}
|
||||
|
||||
export async function fetchAgentMessages(agentId: string): Promise<AgentMessage[]> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/messages`);
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/messages`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.messages || [];
|
||||
}
|
||||
|
||||
export async function fetchErrorAgents(): Promise<ManagedAgent[]> {
|
||||
const res = await fetch(`${getBase()}/v1/agents/errors`);
|
||||
const res = await apiFetch(`/v1/agents/errors`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.agents || [];
|
||||
@@ -790,7 +834,7 @@ export interface ToolInfo {
|
||||
}
|
||||
|
||||
export async function fetchAvailableTools(): Promise<ToolInfo[]> {
|
||||
const res = await fetch(`${getBase()}/v1/tools`);
|
||||
const res = await apiFetch(`/v1/tools`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.tools || [];
|
||||
@@ -800,7 +844,7 @@ export async function saveToolCredentials(
|
||||
toolName: string,
|
||||
credentials: Record<string, string>,
|
||||
): Promise<void> {
|
||||
const res = await fetch(`${getBase()}/v1/tools/${toolName}/credentials`, {
|
||||
const res = await apiFetch(`/v1/tools/${toolName}/credentials`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(credentials),
|
||||
@@ -824,26 +868,26 @@ export interface AgentTraceDetail {
|
||||
}
|
||||
|
||||
export async function fetchLearningLog(agentId: string): Promise<LearningLogEntry[]> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/learning`);
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/learning`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.learning_log || [];
|
||||
}
|
||||
|
||||
export async function triggerLearning(agentId: string): Promise<void> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/learning/run`, { method: 'POST' });
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/learning/run`, { method: 'POST' });
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
}
|
||||
|
||||
export async function fetchAgentTraces(agentId: string, limit = 20): Promise<AgentTrace[]> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/traces?limit=${limit}`);
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/traces?limit=${limit}`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.traces || [];
|
||||
}
|
||||
|
||||
export async function fetchAgentTrace(agentId: string, traceId: string): Promise<AgentTraceDetail> {
|
||||
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/traces/${traceId}`);
|
||||
const res = await apiFetch(`/v1/managed-agents/${agentId}/traces/${traceId}`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
@@ -911,13 +955,13 @@ export interface MemoryConfig {
|
||||
}
|
||||
|
||||
export async function getMemoryStats(): Promise<MemoryStats> {
|
||||
const res = await fetch(`${getBase()}/v1/memory/stats`);
|
||||
const res = await apiFetch(`/v1/memory/stats`);
|
||||
if (!res.ok) throw new Error('Failed to fetch memory stats');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function searchMemory(query: string, topK: number = 5): Promise<MemorySearchResult[]> {
|
||||
const res = await fetch(`${getBase()}/v1/memory/search`, {
|
||||
const res = await apiFetch(`/v1/memory/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, top_k: topK }),
|
||||
@@ -928,7 +972,7 @@ export async function searchMemory(query: string, topK: number = 5): Promise<Mem
|
||||
}
|
||||
|
||||
export async function storeMemory(content: string, metadata?: Record<string, unknown>): Promise<void> {
|
||||
const res = await fetch(`${getBase()}/v1/memory/store`, {
|
||||
const res = await apiFetch(`/v1/memory/store`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content, metadata }),
|
||||
@@ -937,7 +981,7 @@ export async function storeMemory(content: string, metadata?: Record<string, unk
|
||||
}
|
||||
|
||||
export async function indexMemoryPath(path: string): Promise<{ chunks_indexed: number }> {
|
||||
const res = await fetch(`${getBase()}/v1/memory/index`, {
|
||||
const res = await apiFetch(`/v1/memory/index`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path }),
|
||||
@@ -947,7 +991,7 @@ export async function indexMemoryPath(path: string): Promise<{ chunks_indexed: n
|
||||
}
|
||||
|
||||
export async function getMemoryConfig(): Promise<MemoryConfig> {
|
||||
const res = await fetch(`${getBase()}/v1/memory/config`);
|
||||
const res = await apiFetch(`/v1/memory/config`);
|
||||
if (!res.ok) throw new Error('Failed to fetch memory config');
|
||||
return res.json();
|
||||
}
|
||||
@@ -969,19 +1013,19 @@ export interface PendingApproval {
|
||||
}
|
||||
|
||||
export async function fetchPendingApprovals(): Promise<PendingApproval[]> {
|
||||
const res = await fetch(`${getBase()}/v1/approvals/pending`);
|
||||
const res = await apiFetch(`/v1/approvals/pending`);
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.actions || [];
|
||||
}
|
||||
|
||||
export async function approveAction(actionId: string): Promise<void> {
|
||||
const res = await fetch(`${getBase()}/v1/approvals/${actionId}/approve`, { method: 'POST' });
|
||||
const res = await apiFetch(`/v1/approvals/${actionId}/approve`, { method: 'POST' });
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
}
|
||||
|
||||
export async function denyAction(actionId: string): Promise<void> {
|
||||
const res = await fetch(`${getBase()}/v1/approvals/${actionId}/deny`, { method: 'POST' });
|
||||
const res = await apiFetch(`/v1/approvals/${actionId}/deny`, { method: 'POST' });
|
||||
if (!res.ok) throw new Error(`Failed: ${res.status}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,10 @@ export type ThemeMode = 'light' | 'dark' | 'system';
|
||||
interface Settings {
|
||||
theme: ThemeMode;
|
||||
apiUrl: string;
|
||||
// Local server API key (OPENJARVIS_API_KEY). Sent as a Bearer token on
|
||||
// /v1 + /api requests so a key-protected `jarvis serve` doesn't 401 the
|
||||
// frontend (#266). Empty = no auth header (keyless local default).
|
||||
apiKey: string;
|
||||
fontSize: 'small' | 'default' | 'large';
|
||||
defaultModel: string;
|
||||
defaultAgent: string;
|
||||
@@ -82,6 +86,7 @@ function loadSettings(): Settings {
|
||||
const defaults: Settings = {
|
||||
theme: 'system',
|
||||
apiUrl: '',
|
||||
apiKey: '',
|
||||
fontSize: 'default',
|
||||
defaultModel: '',
|
||||
defaultAgent: '',
|
||||
|
||||
@@ -345,6 +345,21 @@ export function SettingsPage() {
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow label="API key" description="Required only if the server was started with an API key">
|
||||
<input
|
||||
type="password"
|
||||
value={settings.apiKey}
|
||||
onChange={(e) => { updateSettings({ apiKey: e.target.value }); showSaved(); }}
|
||||
placeholder="OPENJARVIS_API_KEY"
|
||||
autoComplete="off"
|
||||
className="text-sm px-3 py-1.5 rounded-lg outline-none w-56"
|
||||
style={{
|
||||
background: 'var(--color-bg-secondary)',
|
||||
color: 'var(--color-text)',
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
</Section>
|
||||
|
||||
{/* Inference source */}
|
||||
|
||||
Reference in New Issue
Block a user