Skip to content

feat: add OrcaRouter API-key and PKCE login - #45

Open
lovejones2914-spec wants to merge 1 commit into
gideonsenku:masterfrom
lovejones2914-spec:orcarouter/task-5925
Open

feat: add OrcaRouter API-key and PKCE login#45
lovejones2914-spec wants to merge 1 commit into
gideonsenku:masterfrom
lovejones2914-spec:orcarouter/task-5925

Conversation

@lovejones2914-spec

Copy link
Copy Markdown

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.

  • Provider entry in OrcaRouter/provider.js and OrcaRouter/origins.js
    base_url = https://api.orcarouter.ai/v1, Bearer auth, wire API chat-completions.
  • API-key choice: orcarouter ("OrcaRouter · API key"). The key is stored in the iOS Keychain
    through the repository's existing Env.js helpers (setdata/getdata/hasdata/rmdata); the
    desktop helper writes the same shape into a 0600 credentials file. No second credential store is
    introduced.
  • Connect flow: OAuth 2.0 + PKCE (S256). Flow A — loopback redirect for the desktop
    configuration helper, which has a browser and can bind 127.0.0.1; Flow B — out-of-band code
    for 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, no
bundler, 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 exactly
one AI-related script — ClaudeUsage/claude-usage.js — which reads the user's claude.ai web
session cookie
and calls https://claude.ai/api/organizations/<id>/usage to draw a usage meter. It
is 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 Node
standard-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.js and return the same
CredentialResult. A test asserts that inference and model discovery resolve the identical key and
build 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 401 from the relay is terminal reauthentication: markRejected marks exactly the account and
credential 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 a 0600 JSON 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

Purpose Default
Inference and model catalog https://api.orcarouter.ai/v1
Authorization and code exchange https://www.orcarouter.ai (/auth, /api/v1/auth/keys)

ORCA_BASE_URL sets a shared self-hosted origin; ORCA_AUTH_BASE_URL and ORCA_API_BASE_URL
override 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/keys returns 404. The relay lives at /v1 and the
auth endpoints do not.

Model discovery and capability filtering

The single source of truth is GET <apiBase>/models. When live discovery succeeds it is
authoritative 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.

Entry point Selection rule Live count, 2026-09-11
Text chat / agent ?capability=chat and a text endpoint type (openai/anthropic/gemini/openai-response) 160 of 166 returned
Multimodal understanding chat and architecture.input_modalities contains the attached modality 121
Embeddings endpoint embeddings 5
Image generation endpoint image-generation 6
Video generation endpoint openai-video
Rerank endpoint jina-rerank

The server's ?capability=chat filter is necessary but not sufficient: the live endpoint returns
records 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.
architecture is absent on 34 records; those declare nothing, fail closed, and never appear in a
multimodal list — orcarouter/auto is 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):

ID Endpoints Context Input modalities Reasoning
openai/gpt-5.5 openai, openai-response not advertised file, image, text low, medium, high, xhigh
anthropic/claude-opus-4.8 openai, anthropic, openai-response 1000000 text, image, file
google/gemini-3.5-flash openai, gemini 1048576 text, image, video, file, audio
deepseek/deepseek-v4-pro openai, openai-response 1048576 text
orcarouter/auto openai, openai-response, anthropic, gemini not advertised not declared → text only

Files

File Role
OrcaRouter/origins.js Resolves the two origins; the only place URLs are built.
OrcaRouter/crypto-adapter.js SHA-256, base64url and CSPRNG access in Node and in Scriptable (via the already-vendored crypto-js.min.js).
OrcaRouter/pkce.js PKCE attempt, authorize URL, constant-time state check, code exchange.
OrcaRouter/credentials.js The credential seam: one interface, two adapters, storage, generation-safe 401.
OrcaRouter/models.js Live discovery, capability filters, verified seed.
OrcaRouter/provider.js Provider definition, request builders, selection reconciler.
OrcaRouter/orcarouter.js On-device Scriptable entry point (Flow B).
OrcaRouter/config-server.js, OrcaRouter/ui/ Desktop configuration helper and model selector (Flow A).
OrcaRouter/README.md Provider documentation and evidence table.
README.md, READMEEN.md Usage section in both existing languages.

No dependency was added: SHA-256 and base64url are in every standard library, and the Scriptable path
reuses the crypto-js.min.js bundle the repository already commits. Nothing outside OrcaRouter/
and the two READMEs is touched.

Testing

All of this was run locally on 2026-09-11 against base b4c9c3f.

$ node --test OrcaRouter/test/
# tests 107
# pass 98
# fail 0
# skipped 9      # the live suite, skipped without ORCAROUTER_API_KEY

$ ORCAROUTER_API_KEY=… node --test OrcaRouter/test/
# tests 107
# pass 107
# fail 0
# skipped 0

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 → consent
redirect → GET /cb on a bound 127.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 the
authorize 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:

  • both entries registered and independently usable; same credential shape from both adapters;
  • inference and discovery resolve the same key and build the same request regardless of source;
  • fresh verifier/state per attempt, base64url(sha256(verifier)), unpadded, S256 only;
  • authorize and exchange hit the auth origin; inference and catalog hit the API origin (also with
    split overrides and a shared self-hosted base); HTTPS required off loopback;
  • denial, wrong state, timeout, explicit cancel, switching authentication method, superseded
    attempt, code reuse, 400, 403, 429, network failure, malformed 200;
  • a failing request cannot start a second exchange (a duplicate callback is refused synchronously);
  • pagehide invalidates the generation, clears busy/hint synchronously, and cancels the server
    task with keepalive; a second login starts without remounting;
  • revoked durable key → needsReauth, key retained, no refresh attempted, 401 for another account
    or a stale generation ignored;
  • every capability filter, multimodal fail-closed, undeclared-modality handling, catalog outage
    falling back to the verified seed with reasoning/context/modality intact, bounded/discovery caps,
    stale selection invalidation;
  • verifier and key do not appear in logs, errors, or the browser payload.

Live, through the code paths this PR adds (not a standalone curl):

live catalog:      166 records from https://api.orcarouter.ai/v1/models?capability=chat
text dropdown:     160 of 166 records (6 removed by the local filter)
multimodal:        121 of 160 text models
embedding:         5 models        image generation: 6 models
inference ok:      model=gemini-3.1-flash-lite  reply="ready"
rejected key:      401 → needsReauth, stored key retained, no retry loop
config server:     160 models, key absent from the browser payload

UI

The configuration helper drives the real OrcaRouter/config-server.js page in headless Chromium via
OrcaRouter/ui/evidence.py. The API key is handed to the server once over loopback HTTP and is
asserted absent from the DOM and from the credential status line.

The model listbox is a real role="listbox" with role="option" children; the trigger carries
aria-expanded. The screenshots assert aria-expanded=true, a computed opaque background
(rgb(255,255,255)) and a visible border (1px solid), and that the panel's right edge matches the
trigger's within 0 px (measured 0).

Screenshot Content
auth-methods.png Both entries side by side: masked API-key field + Save/Clear, and "Connect with OrcaRouter" + Cancel.
text-model-dropdown.png Text mode, expanded listbox, 160 live models.
multimodal-model-dropdown.png After attaching an image, the same listbox rebuilt to 121 models that all declare image input.

Automated checks in the same run: the key control is type="password"; both entries are visible and
enabled; pagehide clears busy/hint and permits a second login without remounting; the multimodal
option 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.

Item Source Verified
OpenAI-compatible inference endpoint POST https://api.orcarouter.ai/v1/chat/completions 200, real completion returned
Model-list endpoint GET https://api.orcarouter.ai/v1/models 200, 208 records
Authorization endpoint https://www.orcarouter.ai/auth 200
Code exchange POST https://www.orcarouter.ai/api/v1/auth/keys 200 (fake-server exchange test); https://api.orcarouter.ai/v1/auth/keys confirmed 404
Discovery document https://www.orcarouter.ai/.well-known/openid-configuration 200; advertises S256 and plain; this integration uses only S256
Key management https://www.orcarouter.ai/console 200
Revocation https://www.orcarouter.ai/console/authorized-apps 200
Pricing https://www.orcarouter.ai/pricing 200
Product site https://www.orcarouter.ai 200

Gaps I will not paper over. https://www.orcarouter.ai/terms, /privacy and /docs all return
404, and https://docs.orcarouter.ai/ returns 403. I am therefore not citing a terms URL, a
privacy 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.

  • Maintenance owner: the OrcaRouter team (this PR's author), reachable via the Discord invite
    above.
  • Verification date: 2026-09-11.

Notes for review

  • Authentication changes commonly trigger an unsponsored_surface-style gate. This repository has
    no 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 is
    OrcaRouter/origins.js.
  • ClaudeUsage/claude-usage.js is deliberately unchanged: it consumes a private web-session cookie
    against a Claude-specific usage endpoint that OrcaRouter does not proxy.
  • The fallback seed is the only place OrcaRouter model IDs are hardcoded, it is five entries, every
    one carries endpoint/modality/context metadata, and OrcaRouter/test/models.test.js asserts the
    seed shape and that a live success does not mix it in.

Signed-off-by: lovejones2914-spec <lovejones2914-spec@users.noreply.github.com>
@98liyue

98liyue commented Sep 11, 2026 via email

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants