Skip to content

Absorb outline-br, and fix certificate pinning across multiple requests - #7

Merged
rahmanow merged 1 commit into
masterfrom
claude/repo-setup-features-sv89xo
Aug 28, 2026
Merged

rahmanow merged 1 commit into
masterfrom
claude/repo-setup-features-sv89xo

Conversation

@rahmanow

Copy link
Copy Markdown
Owner

Merges outline-br into this project, and fixes a pinning bug the new tests uncovered.

Why merge them

Both projects listed Outline access keys and rewrote their URLs for a new host. outline-br did it as a published module; ShadowboxKeys did it as a CLI and has since grown most of what outline-br's README listed under "Coming Soon" — mass key management from the terminal, data limits, usage tracking. Keeping two overlapping tools costs more than it earns.

The package keeps the name shadowbox-keys. outline-br stays on npm frozen at 2.0.2, and its repository should get a deprecation note pointing here — I don't have push access to it, so that one is yours (or say the word and I'll request it).

The library API

index.js becomes the package entry point, so everything the CLI does is now reachable from code:

const { listKeys, getUsage, getKeys, OutlineClient } = require('shadowbox-keys');

const keys  = await listKeys(API, { domain: 'vpn.example.com', certSha256 });
const usage = await getUsage(API, { certSha256 });

OutlineClient is exported for the rest — create, rename, delete, per-key and server-wide data limits.

outline-br compatibility

getKeys(managementApiUrl, newDomain) keeps outline-br's signature, its Name -> ss://... output format (including the trimmed /?outline=1 and Noname for unnamed keys) and its printing behaviour. Callers change their import and nothing else:

- const keys = require('outline-br');
- keys(API, '87.65.43.21');
+ const { getKeys } = require('shadowbox-keys');
+ await getKeys(API, '87.65.43.21');

Two additive differences: it now also returns the lines it printed, and takes an optional third argument for certSha256.

Bug fix: pinning broke on the second request

The new integration tests drive a real HTTPS server, and immediately caught a bug in the certificate pinning merged in #4383973007 (#6):

With TLS session resumption, the second connection skips the certificate exchange. getPeerCertificate() returned nothing, so the check compared the configured fingerprint against (none) and rejected the legitimate server. Any command issuing more than one request — add, remove, rename, limit — failed for anyone who had configured a fingerprint.

Reproduced on the CLI before fixing:

$ node shadowboxKey.js rename Alice Alicia     # correct fingerprint, real server
The server presented an unexpected TLS certificate, so the request was not sent.
  expected: e3823f9bb490d35487ee013ec7d23a3662f76b95eb01a40f4851905152f5a584
  received: (none)

My earlier testing missed it by only ever pairing a correct fingerprint with single-request commands, and a wrong fingerprint with multi-request ones — which fails on request one regardless. The fix disables the agent's TLS session cache (maxCachedSessions: 0) so every request completes a full handshake. Cost is a few milliseconds per request; the guarantee is that the check always has a certificate to inspect. It fails closed either way, so no connection was ever wrongly trusted.

A regression test covers it, and I confirmed it fails without the fix.

Also

  • MIT LICENSE and the Contributor Covenant come across from outline-br. The licence is rewritten with this project's own copyright line — outline-br's copy carried an unrelated holder (Kadira Inc.), which looked like a paste artefact rather than anything intended. Worth a look in case you want a different name or year.
  • files added to package.json so a publish ships only index.js, shadowboxKey.js and lib/.
  • CI now syntax-checks index.js and verifies the module's exports load.

Testing

39 tests, all passing. The 8 new ones are true integration tests: they generate a throwaway self-signed certificate, start a real HTTPS server, and exercise listKeys, getUsage, getKeys, host rewriting, and pinning in both the matching and mismatched cases — including the multi-request regression. They skip cleanly where openssl is unavailable.

Separately, I ran every code sample in the new README section against a mock Management API to confirm the documented shapes and outputs are accurate, and checked npm pack ships the intended files.


Generated by Claude Code

outline-br and ShadowboxKeys both listed Outline access keys and rewrote
their URLs for a new host; ShadowboxKeys had since grown most of
outline-br's roadmap. Merge them so there is one project.

- index.js exposes the library outline-br provided: listKeys() and
  getUsage() return structured data, and getKeys() is kept
  signature-compatible with outline-br, same output format and printing,
  additionally returning the lines and accepting certSha256.
- package main points at the library; the CLI stays the bin entry, and
  files/ limits what gets packed.
- MIT LICENSE and the Contributor Covenant come across from outline-br.
  The licence is rewritten with the project's own copyright line rather
  than copying outline-br's, which carried an unrelated holder.
- README documents module usage and how to migrate from outline-br.

Fix certificate pinning across multiple requests
------------------------------------------------
The new integration tests, which drive a real HTTPS server, caught a bug
in the pinning merged in #6: with TLS session resumption the second
connection skips the certificate exchange, so getPeerCertificate()
returned nothing and the check rejected the legitimate server. Every
command issuing more than one request (add, remove, rename, limit) was
affected whenever a fingerprint was configured; the earlier testing
missed it by only pairing correct fingerprints with single-request
commands. Disabling the agent's TLS session cache forces a full
handshake per request. Covered by a regression test that fails without
the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJR2DDBimsijgYgZS3bUS8
@rahmanow
rahmanow merged commit ce83bb4 into master Aug 28, 2026
4 checks passed
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