pennyrouter 0.3.10 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +98 -39
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -642,7 +642,16 @@ export async function install(flags = {}) {
|
|
|
642
642
|
// key against a store this install never talks to. Ask for the gateway, mint there, and
|
|
643
643
|
// skip the claim entirely.
|
|
644
644
|
gatewayBaseUrl = await resolvePrivateGatewayUrl(flags);
|
|
645
|
-
|
|
645
|
+
const launchConfig = await loadPennyConfig();
|
|
646
|
+
const globalAuth = await readClaudeCodeGatewayAuth();
|
|
647
|
+
const storedGateway = launchConfig.gateway_base_url || globalAuth.gatewayBaseUrl;
|
|
648
|
+
const storedKey = storedGateway && sameGatewayBaseUrl(gatewayBaseUrl, storedGateway)
|
|
649
|
+
? (launchConfig.api_key || globalAuth.apiKey)
|
|
650
|
+
: null;
|
|
651
|
+
claim = {
|
|
652
|
+
api_key: await obtainPrivateGatewayKey(flags, gatewayBaseUrl, storedKey),
|
|
653
|
+
gateway_base_url: gatewayBaseUrl,
|
|
654
|
+
};
|
|
646
655
|
|
|
647
656
|
const claudeProviders = await selectClaudeFundingProviders(flags, needsAuth);
|
|
648
657
|
const codexProviders = await selectCodexFundingProviders(flags, needsAuth);
|
|
@@ -661,48 +670,86 @@ export async function install(flags = {}) {
|
|
|
661
670
|
}));
|
|
662
671
|
}
|
|
663
672
|
} else {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
673
|
+
// Re-running the ordinary installer should refresh local harness configuration, not mint
|
|
674
|
+
// another machine key. Reuse a key already saved for this same gateway; an explicit gateway
|
|
675
|
+
// URL still wins, and a URL change deliberately falls through to fresh authorization.
|
|
676
|
+
const launchConfig = await loadPennyConfig();
|
|
677
|
+
const globalAuth = await readClaudeCodeGatewayAuth();
|
|
678
|
+
const existingApiKey = flags.pennyKey || launchConfig.api_key || globalAuth.apiKey;
|
|
679
|
+
const existingGateway = launchConfig.gateway_base_url || globalAuth.gatewayBaseUrl
|
|
680
|
+
|| DEFAULT_GATEWAY_BASE_URL;
|
|
681
|
+
const requestedGateway = flags.gatewayBaseUrl || existingGateway;
|
|
682
|
+
const reusableKey = existingApiKey
|
|
683
|
+
&& sameGatewayBaseUrl(requestedGateway, existingGateway)
|
|
684
|
+
? existingApiKey
|
|
685
|
+
: null;
|
|
686
|
+
|
|
687
|
+
if (reusableKey) {
|
|
688
|
+
gatewayBaseUrl = existingGateway;
|
|
689
|
+
claim = { api_key: reusableKey, gateway_base_url: gatewayBaseUrl };
|
|
690
|
+
console.log(`Using existing PennyRouter key for ${gatewayBaseUrl}.`);
|
|
691
|
+
console.log("");
|
|
669
692
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
693
|
+
const claudeProviders = await selectClaudeFundingProviders(flags, needsAuth);
|
|
694
|
+
const codexProviders = await selectCodexFundingProviders(flags, needsAuth);
|
|
695
|
+
claudeAnthropicAuth = await maybeConfigureClaudeAnthropicAuth(
|
|
696
|
+
flags, needsAuth, claudeProviders.has("subscription"));
|
|
697
|
+
funding = await configureUserFunding(
|
|
698
|
+
flags, needsAuth, claudeAnthropicAuth, claudeProviders, codexProviders);
|
|
675
699
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
700
|
+
for (const credential of funding.credentials) {
|
|
701
|
+
await withGatewayContext(gatewayBaseUrl, () => storeUpstreamCredential({
|
|
702
|
+
gatewayBaseUrl,
|
|
703
|
+
apiKey: claim.api_key,
|
|
704
|
+
provider: credential.provider,
|
|
705
|
+
token: credential.token,
|
|
706
|
+
metadata: credential.metadata,
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
const session = await createCliSession({
|
|
711
|
+
appUrl: flags.appUrl || APP_URL,
|
|
712
|
+
harnesses: selected.map((harness) => harness.id),
|
|
713
|
+
accountMode: "existing",
|
|
714
|
+
});
|
|
679
715
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
);
|
|
716
|
+
console.log("Opening your browser to log in and authorize this machine with your PennyRouter account...");
|
|
717
|
+
console.log(`If your browser did not open, visit:\n${session.authorize_url}`);
|
|
718
|
+
if (!flags.noBrowser) openBrowser(session.authorize_url);
|
|
719
|
+
console.log("");
|
|
720
|
+
console.log("Waiting for approval...");
|
|
721
|
+
|
|
722
|
+
claim = await pollForKey(session);
|
|
723
|
+
console.log(`Authorized. Received key ${maskKey(claim.api_key)}.`);
|
|
724
|
+
console.log("");
|
|
725
|
+
|
|
726
|
+
const claudeProviders = await selectClaudeFundingProviders(flags, needsAuth);
|
|
727
|
+
const codexProviders = await selectCodexFundingProviders(flags, needsAuth);
|
|
728
|
+
claudeAnthropicAuth = await maybeConfigureClaudeAnthropicAuth(
|
|
729
|
+
flags, needsAuth, claudeProviders.has("subscription"));
|
|
730
|
+
funding = await configureUserFunding(
|
|
731
|
+
flags, needsAuth, claudeAnthropicAuth, claudeProviders, codexProviders);
|
|
732
|
+
gatewayBaseUrl = flags.gatewayBaseUrl || claim.gateway_base_url;
|
|
733
|
+
if (flags.gatewayBaseUrl) {
|
|
734
|
+
const normalized = normalizeGatewayBaseUrl(flags.gatewayBaseUrl);
|
|
735
|
+
if (!normalized) {
|
|
736
|
+
throw new Error(
|
|
737
|
+
`--gateway-base-url "${flags.gatewayBaseUrl}" is not a usable URL. Use a host like `
|
|
738
|
+
+ "localhost:8000 or https://gateway.example.com.",
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
gatewayBaseUrl = normalized;
|
|
694
742
|
}
|
|
695
|
-
gatewayBaseUrl = normalized;
|
|
696
|
-
}
|
|
697
743
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
744
|
+
for (const credential of funding.credentials) {
|
|
745
|
+
await withGatewayContext(gatewayBaseUrl, () => storeUpstreamCredential({
|
|
746
|
+
gatewayBaseUrl,
|
|
747
|
+
apiKey: claim.api_key,
|
|
748
|
+
provider: credential.provider,
|
|
749
|
+
token: credential.token,
|
|
750
|
+
metadata: credential.metadata,
|
|
751
|
+
}));
|
|
752
|
+
}
|
|
706
753
|
}
|
|
707
754
|
}
|
|
708
755
|
|
|
@@ -926,7 +973,7 @@ async function resolvePrivateGatewayUrl(flags) {
|
|
|
926
973
|
/* The key for a private gateway: an admin-issued one if the user has it, otherwise minted
|
|
927
974
|
from the gateway itself against their email. --private-key wins so an administrator can
|
|
928
975
|
still hand out keys on a gateway that also allows self-service. */
|
|
929
|
-
async function obtainPrivateGatewayKey(flags, gatewayBaseUrl) {
|
|
976
|
+
async function obtainPrivateGatewayKey(flags, gatewayBaseUrl, existingApiKey = null) {
|
|
930
977
|
const issued = (flags.privateKey || "").trim();
|
|
931
978
|
if (issued) {
|
|
932
979
|
if (!issued.startsWith("pr-")) {
|
|
@@ -937,6 +984,12 @@ async function obtainPrivateGatewayKey(flags, gatewayBaseUrl) {
|
|
|
937
984
|
return issued;
|
|
938
985
|
}
|
|
939
986
|
|
|
987
|
+
if (existingApiKey) {
|
|
988
|
+
console.log(`Using existing PennyRouter key for ${gatewayBaseUrl}.`);
|
|
989
|
+
console.log("");
|
|
990
|
+
return existingApiKey;
|
|
991
|
+
}
|
|
992
|
+
|
|
940
993
|
let email = (flags.email || "").trim();
|
|
941
994
|
if (!email) {
|
|
942
995
|
// A shared --config file names the gateway but not who is installing, so the email is
|
|
@@ -985,6 +1038,12 @@ function normalizeGatewayBaseUrl(raw) {
|
|
|
985
1038
|
return withScheme.replace(/\/+$/, "");
|
|
986
1039
|
}
|
|
987
1040
|
|
|
1041
|
+
function sameGatewayBaseUrl(left, right) {
|
|
1042
|
+
const normalizedLeft = normalizeGatewayBaseUrl(left);
|
|
1043
|
+
const normalizedRight = normalizeGatewayBaseUrl(right);
|
|
1044
|
+
return Boolean(normalizedLeft && normalizedRight && normalizedLeft === normalizedRight);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
988
1047
|
/* A gateway that is unreachable (wrong host, wrong port, not running) surfaces from fetch() as
|
|
989
1048
|
an opaque "fetch failed" with no status. Name the gateway we could not reach so the user knows
|
|
990
1049
|
which URL to fix. Real HTTP errors carry their own message and pass through untouched. */
|