pennyrouter 0.3.16 → 0.3.18
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 +22 -3
- package/src/harnesses/codex.js +14 -0
- package/src/harnesses/codex.test.js +24 -2
- package/src/launch.js +58 -18
- package/src/launch.test.js +32 -9
- package/src/session.js +11 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -19,13 +19,14 @@ import {
|
|
|
19
19
|
} from "./harnesses/claude-code.js";
|
|
20
20
|
import { clineHarness } from "./harnesses/cline.js";
|
|
21
21
|
import { codeGptHarness } from "./harnesses/codegpt.js";
|
|
22
|
-
import { codexHarness, detectCodexChatgptAuth } from "./harnesses/codex.js";
|
|
22
|
+
import { codexHarness, detectCodexChatgptAuth, isHostedPennyRouterGateway } from "./harnesses/codex.js";
|
|
23
23
|
import { fetchOpenCodeModels, opencodeHarness } from "./harnesses/opencode.js";
|
|
24
24
|
import { windsurfHarness } from "./harnesses/windsurf.js";
|
|
25
25
|
import { zedHarness } from "./harnesses/zed.js";
|
|
26
26
|
import {
|
|
27
27
|
APP_URL,
|
|
28
28
|
createCliSession,
|
|
29
|
+
fetchGatewayCapabilities,
|
|
29
30
|
forgetAnthropicToken,
|
|
30
31
|
forgetUpstreamCredential,
|
|
31
32
|
maskKey,
|
|
@@ -36,6 +37,14 @@ import {
|
|
|
36
37
|
storeAnthropicToken,
|
|
37
38
|
storeUpstreamCredential,
|
|
38
39
|
} from "./session.js";
|
|
40
|
+
|
|
41
|
+
async function codexNativeAuthAvailable({ gatewayBaseUrl, apiKey, localAuth }) {
|
|
42
|
+
if (!isHostedPennyRouterGateway(gatewayBaseUrl)) return false;
|
|
43
|
+
const capabilities = await fetchGatewayCapabilities({ gatewayBaseUrl, apiKey });
|
|
44
|
+
const hasOpenAiCredential = Array.isArray(capabilities?.upstream_credentials)
|
|
45
|
+
&& capabilities.upstream_credentials.includes("openai_api");
|
|
46
|
+
return Boolean(localAuth || hasOpenAiCredential);
|
|
47
|
+
}
|
|
39
48
|
import {
|
|
40
49
|
backupFile,
|
|
41
50
|
compactHome,
|
|
@@ -832,7 +841,13 @@ export async function install(flags = {}) {
|
|
|
832
841
|
|
|
833
842
|
const harnessNeededAuth = needsAuth.includes(harness);
|
|
834
843
|
const codexSubscriptionAuth = harness.id === "codex"
|
|
835
|
-
?
|
|
844
|
+
? await codexNativeAuthAvailable({
|
|
845
|
+
gatewayBaseUrl: gatewayBaseUrl,
|
|
846
|
+
apiKey: claim.api_key,
|
|
847
|
+
localAuth: harnessNeededAuth
|
|
848
|
+
? funding.codexSubscriptionAuth
|
|
849
|
+
: Boolean(existingRecord?.openai_subscription_auth),
|
|
850
|
+
})
|
|
836
851
|
: false;
|
|
837
852
|
const claudeSubscriptionAuth = harness.id === "claude-code"
|
|
838
853
|
&& Boolean(claudeAnthropicAuth || existingRecord?.anthropic_token_stored);
|
|
@@ -1269,7 +1284,11 @@ async function update(flags) {
|
|
|
1269
1284
|
const claudeSubscriptionAuth = record.harness === "claude-code"
|
|
1270
1285
|
&& Boolean(record.anthropic_token_stored);
|
|
1271
1286
|
const codexSubscriptionAuth = record.harness === "codex"
|
|
1272
|
-
? await
|
|
1287
|
+
? await codexNativeAuthAvailable({
|
|
1288
|
+
gatewayBaseUrl,
|
|
1289
|
+
apiKey,
|
|
1290
|
+
localAuth: await detectCodexChatgptAuth(),
|
|
1291
|
+
})
|
|
1273
1292
|
: false;
|
|
1274
1293
|
|
|
1275
1294
|
if (supportsPennyCommand) {
|
package/src/harnesses/codex.js
CHANGED
|
@@ -103,6 +103,12 @@ export function renderCodexConfig(
|
|
|
103
103
|
text,
|
|
104
104
|
{ apiKey, gatewayBaseUrl, subscriptionAuth, surface = "codex-terminal" },
|
|
105
105
|
) {
|
|
106
|
+
// Native Codex subscription/WebSocket auth is valid only through PennyRouter's hosted
|
|
107
|
+
// cloud gateway. Local, private, proxy, and other custom gateways must always use the
|
|
108
|
+
// ordinary PennyRouter bearer provider, even when Codex itself is logged into ChatGPT.
|
|
109
|
+
// Keep this guard at the final renderer so every install/update/desktop path gets the
|
|
110
|
+
// same safety rule regardless of which profile selected it.
|
|
111
|
+
subscriptionAuth = Boolean(subscriptionAuth && isHostedPennyRouterGateway(gatewayBaseUrl));
|
|
106
112
|
const clean = removeManagedCodexConfig(text, { removeRootProvider: true }).trimEnd();
|
|
107
113
|
if (new RegExp(`^\\s*\\[model_providers\\.${escapeRegex(PROVIDER_ID)}\\]\\s*$`, "m").test(clean)) {
|
|
108
114
|
throw new Error(
|
|
@@ -142,6 +148,14 @@ export function renderCodexConfig(
|
|
|
142
148
|
return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trim()}\n`;
|
|
143
149
|
}
|
|
144
150
|
|
|
151
|
+
export function isHostedPennyRouterGateway(gatewayBaseUrl) {
|
|
152
|
+
try {
|
|
153
|
+
return new URL(String(gatewayBaseUrl)).hostname.toLowerCase() === "api.pennyrouter.com";
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
145
159
|
export function removeManagedCodexConfig(text, { removeRootProvider = false } = {}) {
|
|
146
160
|
let out = String(text || "").replace(
|
|
147
161
|
new RegExp(`${escapeRegex(MANAGED_START)}[\\s\\S]*?${escapeRegex(MANAGED_END)}\\n?`, "g"),
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
codexUsesChatgptAuth,
|
|
9
9
|
removeManagedCodexConfig,
|
|
10
10
|
renderCodexConfig,
|
|
11
|
+
isHostedPennyRouterGateway,
|
|
11
12
|
} from "./codex.js";
|
|
12
13
|
|
|
13
14
|
function testAuthDetection() {
|
|
@@ -21,13 +22,13 @@ function testSubscriptionProviderIsRootSafe() {
|
|
|
21
22
|
const original = `model = "gpt-test"\n\n[features]\nplugins = false\n`;
|
|
22
23
|
const next = renderCodexConfig(original, {
|
|
23
24
|
apiKey: "pr-test-secret",
|
|
24
|
-
gatewayBaseUrl: "
|
|
25
|
+
gatewayBaseUrl: "https://api.pennyrouter.com/",
|
|
25
26
|
subscriptionAuth: true,
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
assert.ok(next.indexOf('model_provider = "pennyrouter"') < next.indexOf("[features]"));
|
|
29
30
|
assert.ok(next.includes('[model_providers.pennyrouter]'));
|
|
30
|
-
assert.ok(next.includes('base_url = "
|
|
31
|
+
assert.ok(next.includes('base_url = "https://api.pennyrouter.com/v1"'));
|
|
31
32
|
assert.ok(next.includes("requires_openai_auth = true"));
|
|
32
33
|
assert.ok(next.includes('"x-pennyrouter-key" = "pr-test-secret"'));
|
|
33
34
|
assert.ok(next.includes('"x-pennyrouter-harness" = "codex"'));
|
|
@@ -36,6 +37,26 @@ function testSubscriptionProviderIsRootSafe() {
|
|
|
36
37
|
assert.equal((next.match(/# --- PennyRouter managed Codex provider ---/g) || []).length, 1);
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function testPrivateAndProxyGatewaysNeverEnableNativeSubscription() {
|
|
41
|
+
assert.equal(isHostedPennyRouterGateway("https://api.pennyrouter.com"), true);
|
|
42
|
+
for (const gateway of [
|
|
43
|
+
"http://localhost:8400",
|
|
44
|
+
"https://private.example.test",
|
|
45
|
+
"https://openrouter.example.test",
|
|
46
|
+
]) {
|
|
47
|
+
assert.equal(isHostedPennyRouterGateway(gateway), false);
|
|
48
|
+
const next = renderCodexConfig("", {
|
|
49
|
+
apiKey: "pr-private",
|
|
50
|
+
gatewayBaseUrl: gateway,
|
|
51
|
+
subscriptionAuth: true,
|
|
52
|
+
});
|
|
53
|
+
assert.ok(next.includes('model_provider = "pennyrouter"'));
|
|
54
|
+
assert.ok(next.includes('experimental_bearer_token = "pr-private"'));
|
|
55
|
+
assert.ok(!next.includes("requires_openai_auth"));
|
|
56
|
+
assert.ok(next.includes('supports_websockets = false'));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
39
60
|
function testLegacyMigrationAndIdempotence() {
|
|
40
61
|
const legacy = `[features]\nplugins = false\nopenai_base_url = "http://old/v1"\nopenai_api_key = "pr-old"\n`;
|
|
41
62
|
const once = renderCodexConfig(legacy, {
|
|
@@ -115,6 +136,7 @@ async function testReinstallPreservesRecordedProvider() {
|
|
|
115
136
|
|
|
116
137
|
testAuthDetection();
|
|
117
138
|
testSubscriptionProviderIsRootSafe();
|
|
139
|
+
testPrivateAndProxyGatewaysNeverEnableNativeSubscription();
|
|
118
140
|
testLegacyMigrationAndIdempotence();
|
|
119
141
|
testProviderCollisionFailsClosed();
|
|
120
142
|
testUninstallLeavesUserConfig();
|
package/src/launch.js
CHANGED
|
@@ -24,7 +24,11 @@ import {
|
|
|
24
24
|
writeClaudeCodeModelPresentation,
|
|
25
25
|
} from "./harnesses/claude-code.js";
|
|
26
26
|
import { renderCodexConfig } from "./harnesses/codex.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
OPENCODE_MODEL_CHOICES,
|
|
29
|
+
fetchOpenCodeModels,
|
|
30
|
+
stripPersistentOpenCodeProvider,
|
|
31
|
+
} from "./harnesses/opencode.js";
|
|
28
32
|
import { atomicWrite, homeDir, readJsonFile, readText, writeJsonFile } from "./state.js";
|
|
29
33
|
|
|
30
34
|
const CONFIG_VERSION = 1;
|
|
@@ -386,16 +390,31 @@ export async function installPennyRuntime({ updatePath = true } = {}) {
|
|
|
386
390
|
}
|
|
387
391
|
|
|
388
392
|
export function renderCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth }) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
393
|
+
// Private/proxy/custom gateways never use Codex's native subscription/WebSocket lane.
|
|
394
|
+
// Apply the same hosted-only rule as the Codex config renderer at this launch-profile
|
|
395
|
+
// boundary, since this path is what bare `penny codex` selects at runtime.
|
|
396
|
+
try {
|
|
397
|
+
subscriptionAuth = Boolean(
|
|
398
|
+
subscriptionAuth && new URL(String(gatewayBaseUrl)).hostname.toLowerCase() === "api.pennyrouter.com",
|
|
399
|
+
);
|
|
400
|
+
} catch {
|
|
401
|
+
subscriptionAuth = false;
|
|
402
|
+
}
|
|
403
|
+
if (subscriptionAuth) {
|
|
404
|
+
return [
|
|
405
|
+
CODEX_PROFILE_MARKER,
|
|
406
|
+
'model_provider = "openai"',
|
|
407
|
+
`openai_base_url = ${JSON.stringify(String(gatewayBaseUrl).replace(/\/+$/, ""))}`,
|
|
408
|
+
"",
|
|
409
|
+
"[tui]",
|
|
410
|
+
"terminal_title = []",
|
|
411
|
+
"",
|
|
412
|
+
].join("\n");
|
|
413
|
+
}
|
|
414
|
+
const authLines = [
|
|
415
|
+
'env_key = "PENNYROUTER_API_KEY"',
|
|
416
|
+
'http_headers = { "x-pennyrouter-title-session" = "PENNYROUTER_TITLE_SESSION_ID", "x-pennyrouter-harness" = "codex", "x-pennyrouter-surface" = "codex-terminal" }',
|
|
417
|
+
];
|
|
399
418
|
return [
|
|
400
419
|
CODEX_PROFILE_MARKER,
|
|
401
420
|
'model_provider = "pennyrouter"',
|
|
@@ -998,12 +1017,15 @@ function openCodeModelCachePath(profile) {
|
|
|
998
1017
|
}
|
|
999
1018
|
|
|
1000
1019
|
/**
|
|
1001
|
-
* Model catalog for one OpenCode launch
|
|
1020
|
+
* Model catalog for one OpenCode launch.
|
|
1002
1021
|
*
|
|
1003
1022
|
* The gateway is the authority, but it is not always reachable: a private gateway sits
|
|
1004
|
-
* behind a VPN the user is not always on.
|
|
1005
|
-
* profile
|
|
1006
|
-
*
|
|
1023
|
+
* behind a VPN the user is not always on. Fall back to the last catalog seen for this
|
|
1024
|
+
* profile, which is why the cache is per profile rather than global -- a cloud catalog
|
|
1025
|
+
* must never stand in for a private one.
|
|
1026
|
+
*
|
|
1027
|
+
* Discovery never blocks the launch. A stale or built-in list still lets the user work,
|
|
1028
|
+
* whereas refusing to start leaves them with no OpenCode at all over a model menu.
|
|
1007
1029
|
*/
|
|
1008
1030
|
export async function resolveOpenCodeLaunchModels(config) {
|
|
1009
1031
|
// One-time migration off the install-time provider block. Harmless once it is gone.
|
|
@@ -1029,10 +1051,12 @@ export async function resolveOpenCodeLaunchModels(config) {
|
|
|
1029
1051
|
);
|
|
1030
1052
|
return cached.models;
|
|
1031
1053
|
}
|
|
1032
|
-
|
|
1033
|
-
`PennyRouter could not
|
|
1034
|
-
+ `
|
|
1054
|
+
console.error(
|
|
1055
|
+
`PennyRouter: could not reach ${config.gateway_base_url} (${error.message}) and no `
|
|
1056
|
+
+ `catalog is cached for the "${config.profile}" profile. Falling back to the built-in `
|
|
1057
|
+
+ "model list; some entries may not be available on this gateway.",
|
|
1035
1058
|
);
|
|
1059
|
+
return OPENCODE_MODEL_CHOICES;
|
|
1036
1060
|
}
|
|
1037
1061
|
}
|
|
1038
1062
|
|
|
@@ -1106,6 +1130,12 @@ export function buildPennyInvocation(harness, args, config, models = null) {
|
|
|
1106
1130
|
// identity. Give both sides a launch-local join token so title warmth cannot bleed between
|
|
1107
1131
|
// two Codex TUIs using the same Penny key.
|
|
1108
1132
|
env.PENNYROUTER_TITLE_SESSION_ID = randomUUID();
|
|
1133
|
+
if (resolveSubscriptionAuth(config, "codex")) {
|
|
1134
|
+
// The built-in OpenAI provider must retain Authorization for Codex's native OAuth.
|
|
1135
|
+
// OpenAI-Project is a standard built-in-provider header and carries the existing Penny
|
|
1136
|
+
// credential only as far as our gateway, which removes it before relaying upstream.
|
|
1137
|
+
env.OPENAI_PROJECT = config.api_key;
|
|
1138
|
+
}
|
|
1109
1139
|
return {
|
|
1110
1140
|
command: "codex",
|
|
1111
1141
|
args: ["--profile", codexProfileName(config.profile), ...args],
|
|
@@ -1197,6 +1227,16 @@ export async function runPenny(argv = process.argv.slice(2)) {
|
|
|
1197
1227
|
}
|
|
1198
1228
|
return runCodexDesktop(config, { logs, mode });
|
|
1199
1229
|
}
|
|
1230
|
+
if (harness === "codex") {
|
|
1231
|
+
// Re-render on every launch so an upgraded Penny command moves existing subscription
|
|
1232
|
+
// profiles onto Codex's built-in OpenAI provider without requiring a reinstall. Profiles
|
|
1233
|
+
// using API/proxy/OpenRouter funding retain the custom HTTP provider for now.
|
|
1234
|
+
await writeCodexLaunchProfile({
|
|
1235
|
+
gatewayBaseUrl: config.gateway_base_url,
|
|
1236
|
+
subscriptionAuth: resolveSubscriptionAuth(config, "codex"),
|
|
1237
|
+
profile: config.profile,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1200
1240
|
const models = harness === "opencode" ? await resolveOpenCodeLaunchModels(config) : null;
|
|
1201
1241
|
const invocation = buildPennyInvocation(harness, argv.slice(1), config, models);
|
|
1202
1242
|
const child = spawn(invocation.command, invocation.args, {
|
package/src/launch.test.js
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
selectClaudeDesktopNativeMode,
|
|
31
31
|
renderCodexLaunchProfile,
|
|
32
32
|
extractProfileArgs,
|
|
33
|
+
resolveOpenCodeLaunchModels,
|
|
34
|
+
buildOpenCodeSessionConfig,
|
|
33
35
|
resolvePennyProfile,
|
|
34
36
|
setPennyProfile,
|
|
35
37
|
savePennyConfig,
|
|
@@ -185,20 +187,15 @@ async function main() {
|
|
|
185
187
|
const desktopConfig = fs.readFileSync(path.join(desktopHome, "config.toml"), "utf8");
|
|
186
188
|
assert.match(desktopConfig, /^model_provider = "pennyrouter"/);
|
|
187
189
|
assert.match(desktopConfig, /\[model_providers\.pennyrouter\]/);
|
|
188
|
-
assert.match(desktopConfig, /
|
|
190
|
+
assert.match(desktopConfig, /experimental_bearer_token = "pr-super-secret"/);
|
|
189
191
|
assert.match(desktopConfig, /"x-pennyrouter-surface" = "codex-desktop"/);
|
|
190
192
|
assert.equal(fs.readFileSync(path.join(home, ".codex", "config.toml"), "utf8"), nativeCodex);
|
|
191
193
|
fs.rmSync(desktopHome, { recursive: true, force: true });
|
|
192
194
|
const codexProfile = fs.readFileSync(codexLaunchProfilePath(), "utf8");
|
|
193
|
-
assert.
|
|
194
|
-
assert.match(codexProfile,
|
|
195
|
-
|
|
196
|
-
assert.match(codexProfile,
|
|
197
|
-
/http_headers = \{ "x-pennyrouter-harness" = "codex", "x-pennyrouter-surface" = "codex-terminal" \}/);
|
|
195
|
+
assert.match(codexProfile, /model_provider = "pennyrouter"/);
|
|
196
|
+
assert.match(codexProfile, /base_url = "https:\/\/gateway\.example\/v1"/);
|
|
197
|
+
assert.ok(codexProfile.includes("[model_providers.pennyrouter]"));
|
|
198
198
|
assert.ok(codexProfile.includes("[tui]\nterminal_title = []"));
|
|
199
|
-
assert.ok(codexProfile.includes('"x-pennyrouter-title-session" = "PENNYROUTER_TITLE_SESSION_ID"'));
|
|
200
|
-
assert.ok(codexProfile.includes('"x-pennyrouter-harness" = "codex"'));
|
|
201
|
-
assert.ok(codexProfile.includes('"x-pennyrouter-surface" = "codex-terminal"'));
|
|
202
199
|
assert.ok(!codexProfile.includes("pr-super-secret"));
|
|
203
200
|
assert.ok(!rc.includes("pr-super-secret"));
|
|
204
201
|
|
|
@@ -228,6 +225,7 @@ async function main() {
|
|
|
228
225
|
const codex = buildPennyInvocation("codex", ["resume", "--last"], config);
|
|
229
226
|
assert.deepEqual(codex.args, ["--profile", "pennyrouter-cloud", "resume", "--last"]);
|
|
230
227
|
assert.equal(codex.env.PENNYROUTER_API_KEY, "pr-super-secret");
|
|
228
|
+
assert.equal(codex.env.OPENAI_PROJECT, "pr-super-secret");
|
|
231
229
|
const opencode = buildPennyInvocation("opencode", ["--continue"], config);
|
|
232
230
|
assert.equal(opencode.env.PENNYROUTER_API_KEY, "pr-super-secret");
|
|
233
231
|
assert.deepEqual(opencode, {
|
|
@@ -372,6 +370,31 @@ async function testProfiles() {
|
|
|
372
370
|
assert.deepEqual(invocation.args, ["--profile", "pennyrouter-private"]);
|
|
373
371
|
assert.ok(codexLaunchProfilePath("private").endsWith("pennyrouter-private.config.toml"));
|
|
374
372
|
assert.notEqual(codexLaunchProfilePath("private"), codexLaunchProfilePath("cloud"));
|
|
373
|
+
|
|
374
|
+
// An unreachable gateway must not prevent OpenCode from starting: a built-in model
|
|
375
|
+
// list is worse than a fresh one, but far better than no editor.
|
|
376
|
+
const offline = { gateway_base_url: "http://127.0.0.1:1", api_key: "pr-x", profile: "cloud" };
|
|
377
|
+
const fallback = await resolveOpenCodeLaunchModels(offline);
|
|
378
|
+
assert.ok(fallback.length > 0);
|
|
379
|
+
|
|
380
|
+
// The launch config carries this profile's gateway and key, so two profiles can run
|
|
381
|
+
// side by side without either writing to OpenCode's shared config file.
|
|
382
|
+
const session = buildOpenCodeSessionConfig(
|
|
383
|
+
{ gateway_base_url: "https://gw.example/", api_key: "pr-private" },
|
|
384
|
+
[{ id: "gpt-4o", name: "gpt-4o" }],
|
|
385
|
+
);
|
|
386
|
+
assert.equal(session.provider.pennyrouter.options.baseURL, "https://gw.example/v1");
|
|
387
|
+
assert.equal(session.provider.pennyrouter.options.apiKey, "pr-private");
|
|
388
|
+
assert.deepEqual(Object.keys(session.provider.pennyrouter.models), ["gpt-4o"]);
|
|
389
|
+
|
|
390
|
+
const opencodeInvocation = buildPennyInvocation(
|
|
391
|
+
"opencode", [], { ...resolvePennyProfile(config, "private"), harnesses: { opencode: { enabled: true } } },
|
|
392
|
+
[{ id: "gpt-4o", name: "gpt-4o" }],
|
|
393
|
+
);
|
|
394
|
+
assert.equal(
|
|
395
|
+
JSON.parse(opencodeInvocation.env.OPENCODE_CONFIG_CONTENT).provider.pennyrouter.options.apiKey,
|
|
396
|
+
"pr-private",
|
|
397
|
+
);
|
|
375
398
|
}
|
|
376
399
|
|
|
377
400
|
main().catch((error) => {
|
package/src/session.js
CHANGED
|
@@ -71,6 +71,17 @@ export async function storeUpstreamCredential({ gatewayBaseUrl, apiKey, provider
|
|
|
71
71
|
return response.json();
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export async function fetchGatewayCapabilities({ gatewayBaseUrl, apiKey }) {
|
|
75
|
+
const url = `${gatewayBaseUrl.replace(/\/+$/, "")}/v1/auth/capabilities`;
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(url, { headers: { authorization: `Bearer ${apiKey}` } });
|
|
78
|
+
if (!response.ok) return null;
|
|
79
|
+
return await response.json();
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
// Mint a key on a private gateway that has self-service provisioning enabled. The hosted
|
|
75
86
|
// browser claim mints against pennyrouter.com's key store, which a private gateway does not
|
|
76
87
|
// share, so this is the equivalent step for a self-hosted install. A gateway that has not
|