feat: add OrcaRouter API-key and PKCE login - #45
Open
lovejones2914-spec wants to merge 1 commit into
Open
Conversation
Signed-off-by: lovejones2914-spec <lovejones2914-spec@users.noreply.github.com>
|
这是来自QQ邮箱的假期自动回复邮件。您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds OrcaRouter as a first-class model provider, with two independent
ways to authenticate. Both produce one ordinary OrcaRouter API key owned by the user.
OrcaRouter/provider.jsandOrcaRouter/origins.js—base_url = https://api.orcarouter.ai/v1, Bearer auth, wire APIchat-completions.orcarouter("OrcaRouter · API key"). The key is stored in the iOS Keychainthrough the repository's existing
Env.jshelpers (setdata/getdata/hasdata/rmdata); thedesktop helper writes the same shape into a
0600credentials file. No second credential store isintroduced.
configuration helper, which has a browser and can bind
127.0.0.1; Flow B — out-of-band codefor the Scriptable entry point, chosen because iOS has no way to receive a redirect.
Affiliation disclosure: I'm an engineer on the OrcaRouter team. This work is made on behalf of
OrcaRouter.
OrcaRouter is an OpenAI-compatible AI gateway built for both models and agents, with adaptive
routing, automatic failover, zero-markup inference, observability, guardrails, and agent-tool
governance. It also runs gateway-level, zero-trust security for AI agents on the same endpoint —
screening every prompt/response and governing every tool call on a default-deny basis, with no
application code changes.
Discord: discord.gg/YEubt8enRA · X: @OrcaRouter
Why this repository needed an architecture decision first
This repository is a collection of Scriptable (iOS) widgets: plain JavaScript, no
package.json, nobundler, no CI, no test runner, no provider registry and no i18n catalogs. Scanning every
.js(excluding the vendored
crypto-js.min.js) for request construction and credential use found exactlyone AI-related script —
ClaudeUsage/claude-usage.js— which reads the user'sclaude.aiwebsession cookie and calls
https://claude.ai/api/organizations/<id>/usageto draw a usage meter. Itis not an LLM request, it is not an OpenAI-compatible route, and OrcaRouter does not serve it, so
that script is untouched and its behaviour is unchanged.
There is no chat, agent, embedding, rerank, image-generation or video-generation entry point in the
repository today. The integration therefore introduces the provider once, in a shared module,
and exposes it to every entry point the repository can grow, rather than copying auth or catalog
logic per script.
Because the repository has no config UI at all, the change also ships the smallest real surface that
can carry a model selector and both authentication entries:
OrcaRouter/config-server.js, a Nodestandard-library HTTP server with a plain-HTML page. It introduces no dependency and no build step,
and it is what the screenshots below show.
How the credential works
The key belongs to the user, not to this project: it is billed to their OrcaRouter account, listed in
their console, and revocable by them at any time. No client secret is involved — PKCE binds the auth
code to this process, so an intercepted code cannot be redeemed by anyone else.
Both choices are adapters on one interface in
credentials.jsand return the sameCredentialResult. A test asserts that inference and model discovery resolve the identical key andbuild the identical request regardless of which adapter ran — provenance is invisible downstream.
A PKCE-issued key is durable, but it is not a refresh token. OrcaRouter has no refresh grant.
A
401from the relay is terminal reauthentication:markRejectedmarks exactly the account andcredential generation that issued the rejected request, a late failure from an older generation is
discarded as
stale_generation, and the stored key is never deleted before a replacement succeeds.No code path schedules a refresh or invents a refresh grant (asserted in the suite).
Storage: on device the iOS Keychain via
Env.js; on desktop a0600JSON file owned by the helper.The key is never written to a URL, log, error message, telemetry payload, fixture or screenshot.
Two origins, never derived from one another
https://api.orcarouter.ai/v1https://www.orcarouter.ai(/auth,/api/v1/auth/keys)ORCA_BASE_URLsets a shared self-hosted origin;ORCA_AUTH_BASE_URLandORCA_API_BASE_URLoverride the two separately and take precedence. Remote origins must be HTTPS; plain HTTP is allowed
only on loopback. Tests assert that auth and exchange requests only ever target the auth origin and
that inference and discovery only ever target the API origin, including with split overrides.
Verified:
https://api.orcarouter.ai/v1/auth/keysreturns 404. The relay lives at/v1and theauth endpoints do not.
Model discovery and capability filtering
The single source of truth is
GET <apiBase>/models. When live discovery succeeds it isauthoritative and the fallback seed is not mixed into it. Filtering reads catalog metadata only —
never a model's name — and a model that does not declare a capability is excluded.
?capability=chatand a text endpoint type (openai/anthropic/gemini/openai-response)architecture.input_modalitiescontains the attached modalityembeddingsimage-generationopenai-videojina-rerankThe server's
?capability=chatfilter is necessary but not sufficient: the live endpoint returnsrecords whose only endpoint types are non-text, and the local filter removes them. That is measured
by the live suite ("160 of 166 records, 6 removed by the local filter") rather than assumed.
architectureis absent on 34 records; those declare nothing, fail closed, and never appear in amultimodal list —
orcarouter/autois one of them.Bounding: 8 s timeout, 2 MiB response cap, 4000 item cap, per-record shape validation. On failure the
verified seed is shown with an explicit degraded flag, and a stored model ID is restored only after
re-checking it against the current capability list.
Verified fallback seed (read from the live endpoint on 2026-09-11; the catalog does not
currently advertise a reasoning field, so the reasoning ladder is verified metadata preserved by
reconcileWithVerifiedMetadata, and a live refresh cannot strip it):openai/gpt-5.5anthropic/claude-opus-4.8google/gemini-3.5-flashdeepseek/deepseek-v4-proorcarouter/autoFiles
OrcaRouter/origins.jsOrcaRouter/crypto-adapter.jscrypto-js.min.js).OrcaRouter/pkce.jsOrcaRouter/credentials.js401.OrcaRouter/models.jsOrcaRouter/provider.jsOrcaRouter/orcarouter.jsOrcaRouter/config-server.js,OrcaRouter/ui/OrcaRouter/README.mdREADME.md,READMEEN.mdNo dependency was added: SHA-256 and base64url are in every standard library, and the Scriptable path
reuses the
crypto-js.min.jsbundle the repository already commits. Nothing outsideOrcaRouter/and the two READMEs is touched.
Testing
All of this was run locally on 2026-09-11 against base
b4c9c3f.The offline suite is not hash-helper-only. It drives the whole Flow A chain against a real fake
auth server and the app's own real loopback listener:
beginLogin→ authorize URL → consentredirect →
GET /cbon a bound127.0.0.1:<port>→POST /api/v1/auth/keys→ persisted credential.It asserts the exchange carries the verifier and
S256, that the verifier never appears on theauthorize URL and is not the challenge, and that a single-use code cannot be redeemed twice.
Covered behaviour, all offline and all with fake keys/codes:
base64url(sha256(verifier)), unpadded,S256only;split overrides and a shared self-hosted base); HTTPS required off loopback;
state, timeout, explicit cancel, switching authentication method, supersededattempt, code reuse,
400,403,429, network failure, malformed 200;pagehideinvalidates the generation, clears busy/hint synchronously, and cancels the servertask with
keepalive; a second login starts without remounting;needsReauth, key retained, no refresh attempted,401for another accountor a stale generation ignored;
falling back to the verified seed with reasoning/context/modality intact, bounded/discovery caps,
stale selection invalidation;
Live, through the code paths this PR adds (not a standalone curl):
UI
The configuration helper drives the real
OrcaRouter/config-server.jspage in headless Chromium viaOrcaRouter/ui/evidence.py. The API key is handed to the server once over loopback HTTP and isasserted absent from the DOM and from the credential status line.
The model listbox is a real
role="listbox"withrole="option"children; the trigger carriesaria-expanded. The screenshots assertaria-expanded=true, a computed opaque background(
rgb(255,255,255)) and a visible border (1px solid), and that the panel's right edge matches thetrigger's within 0 px (measured 0).
auth-methods.pngtext-model-dropdown.pngmultimodal-model-dropdown.pngAutomated checks in the same run: the key control is
type="password"; both entries are visible andenabled;
pagehideclears busy/hint and permits a second login without remounting; the multimodaloption set is a strict subset of the text option set and contains no text-only model.
Provider evidence
Every link below was fetched on 2026-09-11; nothing here is from memory.
POST https://api.orcarouter.ai/v1/chat/completionsGET https://api.orcarouter.ai/v1/modelshttps://www.orcarouter.ai/authPOST https://www.orcarouter.ai/api/v1/auth/keyshttps://api.orcarouter.ai/v1/auth/keysconfirmed 404https://www.orcarouter.ai/.well-known/openid-configurationS256andplain; this integration uses onlyS256https://www.orcarouter.ai/consolehttps://www.orcarouter.ai/console/authorized-appshttps://www.orcarouter.ai/pricinghttps://www.orcarouter.aiGaps I will not paper over.
https://www.orcarouter.ai/terms,/privacyand/docsall return404, and
https://docs.orcarouter.ai/returns 403. I am therefore not citing a terms URL, aprivacy URL, a legal-entity name, a documentation URL or a routing/resale-authorization document — I
could not verify any of them. If this repository requires those for a provider preset, please tell me
where they are published and I will add them; the integration itself does not depend on them.
above.
Notes for review
unsponsored_surface-style gate. This repository hasno CI and no labels, so there is nothing to check, but if you want a security review of the
credential boundary the relevant file is
OrcaRouter/credentials.js, and the origin policy isOrcaRouter/origins.js.ClaudeUsage/claude-usage.jsis deliberately unchanged: it consumes a private web-session cookieagainst a Claude-specific usage endpoint that OrcaRouter does not proxy.
one carries endpoint/modality/context metadata, and
OrcaRouter/test/models.test.jsasserts theseed shape and that a live success does not mix it in.