@juspay/neurolink 11.29.2 → 11.30.0

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/auth/anthropicOAuth.d.ts +50 -0
  3. package/dist/auth/anthropicOAuth.js +78 -0
  4. package/dist/browser/neurolink.min.js +393 -393
  5. package/dist/cli/commands/proxy.d.ts +2 -0
  6. package/dist/cli/commands/proxy.js +284 -4
  7. package/dist/cli/commands/proxyExpose.d.ts +35 -0
  8. package/dist/cli/commands/proxyExpose.js +252 -0
  9. package/dist/cli/commands/proxyPeer.d.ts +29 -0
  10. package/dist/cli/commands/proxyPeer.js +738 -0
  11. package/dist/cli/commands/proxyShare.d.ts +37 -0
  12. package/dist/cli/commands/proxyShare.js +1080 -0
  13. package/dist/cli/parser.js +7 -1
  14. package/dist/proxy/peerStore.d.ts +52 -0
  15. package/dist/proxy/peerStore.js +324 -0
  16. package/dist/proxy/peerTransport.d.ts +38 -0
  17. package/dist/proxy/peerTransport.js +242 -0
  18. package/dist/proxy/proxyPaths.d.ts +8 -0
  19. package/dist/proxy/proxyPaths.js +55 -17
  20. package/dist/proxy/requestLogger.js +8 -0
  21. package/dist/proxy/residentGrants.d.ts +57 -0
  22. package/dist/proxy/residentGrants.js +393 -0
  23. package/dist/proxy/shareAudit.d.ts +81 -0
  24. package/dist/proxy/shareAudit.js +280 -0
  25. package/dist/proxy/shareContext.d.ts +38 -0
  26. package/dist/proxy/shareContext.js +92 -0
  27. package/dist/proxy/shareGate.d.ts +64 -0
  28. package/dist/proxy/shareGate.js +216 -0
  29. package/dist/proxy/shareGrants.d.ts +115 -0
  30. package/dist/proxy/shareGrants.js +590 -0
  31. package/dist/proxy/shareLease.d.ts +101 -0
  32. package/dist/proxy/shareLease.js +192 -0
  33. package/dist/proxy/shareLedger.d.ts +105 -0
  34. package/dist/proxy/shareLedger.js +406 -0
  35. package/dist/proxy/shareListener.d.ts +60 -0
  36. package/dist/proxy/shareListener.js +143 -0
  37. package/dist/proxy/shareNotes.d.ts +97 -0
  38. package/dist/proxy/shareNotes.js +234 -0
  39. package/dist/proxy/sharePolicy.d.ts +110 -0
  40. package/dist/proxy/sharePolicy.js +366 -0
  41. package/dist/proxy/shareProvisioning.d.ts +110 -0
  42. package/dist/proxy/shareProvisioning.js +237 -0
  43. package/dist/proxy/shareReceipts.d.ts +99 -0
  44. package/dist/proxy/shareReceipts.js +303 -0
  45. package/dist/proxy/shareSigning.d.ts +40 -0
  46. package/dist/proxy/shareSigning.js +78 -0
  47. package/dist/server/routes/claudeProxyRoutes.js +1066 -3
  48. package/dist/types/cli.d.ts +61 -0
  49. package/dist/types/proxy.d.ts +781 -0
  50. package/package.json +2 -1
@@ -72,6 +72,8 @@ export declare function createProxyStartApp(params: {
72
72
  accountAllowlist: AccountAllowlist | undefined;
73
73
  runtimeConfigStore?: ProxyRuntimeConfigStore;
74
74
  updateControlToken?: string;
75
+ /** Port of the gate-only share listener, when one is configured. */
76
+ sharePort?: number;
75
77
  }): Promise<{
76
78
  app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
77
79
  readiness: ProxyReadinessState;
@@ -45,7 +45,26 @@ const PROXY_VERSION = packageJson.version;
45
45
  const PROXY_INTERNAL_ACCOUNT_LABEL = "proxy/internal";
46
46
  const PROXY_INTERNAL_ACCOUNT_TYPE = "internal";
47
47
  const PROXY_TELEMETRY_SCRIPT_PATH = fileURLToPath(new URL("../../../scripts/observability/manage-local-openobserve.sh", import.meta.url));
48
+ /**
49
+ * Requests accepted by the gate-only share listener.
50
+ *
51
+ * Both listeners serve the same Hono app, so the only thing separating a gated
52
+ * request from an ungated one is which socket accepted it. Recovering that from
53
+ * `c.env.incoming.socket.localPort` worked but failed in two directions: an
54
+ * unreadable port answered "not gated", which is fail-open on the one listener
55
+ * that must never fail open, and a share port configured equal to the main port
56
+ * answered "gated" for the operator's own untokened traffic.
57
+ *
58
+ * Stamping the Request as the share listener hands it to the app settles both.
59
+ * The mark is applied by the accepting listener before any handler runs, it
60
+ * says nothing about ports, and nothing a client sends can add or remove it. A
61
+ * `WeakSet` keyed on the Request holds it for exactly as long as the request
62
+ * object lives and not a moment longer.
63
+ */
64
+ const gatedShareRequests = new WeakSet();
48
65
  const PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS = 5_000;
66
+ /** How long shutdown waits on the share listener before moving on. */
67
+ const SHARE_LISTENER_CLOSE_TIMEOUT_MS = 10_000;
49
68
  const LEGACY_STATUS_ACCOUNT_CACHE_TTL_MS = 5_000;
50
69
  const PROXY_STATUS_TOKEN_READ_TIMEOUT_MS = 2_000;
51
70
  const PROXY_STATUS_RECONCILE_TIMEOUT_MS = 750;
@@ -1033,6 +1052,39 @@ async function createProxyNeurolinkRuntime(logsDir) {
1033
1052
  logsDir: logsDir ?? join(homedir(), ".neurolink", "logs"),
1034
1053
  };
1035
1054
  }
1055
+ /**
1056
+ * Replace account identity in a `/status` payload with a stable placeholder.
1057
+ *
1058
+ * The shape is preserved — same rows, same counters — so status tooling keeps
1059
+ * working and only the names go away. Callers that legitimately need the names
1060
+ * present the update-control token.
1061
+ */
1062
+ function redactStatusAccounts(rows, allowed) {
1063
+ if (allowed) {
1064
+ return rows;
1065
+ }
1066
+ return rows.map((row, index) => ({
1067
+ ...row,
1068
+ label: `account-${index + 1}`,
1069
+ ...(row.email !== undefined ? { email: null } : {}),
1070
+ }));
1071
+ }
1072
+ /**
1073
+ * The same treatment for the configured/effective primary account block.
1074
+ *
1075
+ * Named fields rather than "every non-empty string": `source` is a discriminant
1076
+ * (`configured` | `fallback`), not an identity, and blanking it both left the
1077
+ * value outside its own union and hid the one thing a caller reads this block
1078
+ * for — whether the configured primary is the account actually in use.
1079
+ */
1080
+ function redactStatusPrimaryAccount(primary) {
1081
+ return {
1082
+ configured: primary.configured === null ? null : "redacted",
1083
+ key: primary.key === null ? null : "redacted",
1084
+ label: primary.label === null ? null : "redacted",
1085
+ source: primary.source,
1086
+ };
1087
+ }
1036
1088
  function registerProxyRequestTracking(app, requestMetadata, readiness) {
1037
1089
  const trackingHandler = async (c, next) => {
1038
1090
  const startedMonotonicMs = performance.now();
@@ -1061,6 +1113,10 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
1061
1113
  : beginProxyRequest();
1062
1114
  const finish = () => {
1063
1115
  finishActivity();
1116
+ // Borrowed traffic holds a concurrency slot for the lifetime of the
1117
+ // response body, so it is released here rather than when the handler
1118
+ // returns. Idempotent — a request can finish more than one way.
1119
+ metadata.shareRelease?.();
1064
1120
  requestMetadata.delete(c.req.raw);
1065
1121
  };
1066
1122
  // The route adapter populates model/stream/toolCount after parsing. Omit
@@ -1136,6 +1192,11 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
1136
1192
  // Keep metadata available to app.onError, which records the client-facing
1137
1193
  // failure with the same request ID before deleting the WeakMap entry.
1138
1194
  finishActivity();
1195
+ // A handler that threw never produced a body for `trackProxyResponse` to
1196
+ // follow, so `finish` will not run and the borrowed-traffic slot (and its
1197
+ // coin hold) would be held until the process restarts. Both releases are
1198
+ // idempotent, so the double call on paths that do reach `finish` is free.
1199
+ metadata.shareRelease?.();
1139
1200
  logProxyLifecycleEvent({
1140
1201
  event: "request_terminal",
1141
1202
  requestId: metadata.requestId,
@@ -1174,8 +1235,24 @@ export async function createProxyStartApp(params) {
1174
1235
  const { createGeminiProxyRoutes } = await import("../../server/routes/geminiProxyRoutes.js");
1175
1236
  const { logBodyCapture, logRequest } = await import("../../proxy/requestLogger.js");
1176
1237
  const { recordFinalError } = await import("../../proxy/usageStats.js");
1238
+ const { admitInboundShareRequest, isGrantRequiredByEnv } = await import("../../proxy/shareGate.js");
1239
+ const { runWithShareContext } = await import("../../proxy/shareContext.js");
1177
1240
  const { Hono } = await import("hono");
1178
1241
  const app = new Hono();
1242
+ /**
1243
+ * Is this request arriving on the gate-only share listener?
1244
+ *
1245
+ * Decided by the **local port the connection was accepted on**, which no
1246
+ * client can influence. A header or an address check could not do this job:
1247
+ * cloudflared and every reverse proxy connect from 127.0.0.1, so tunnelled
1248
+ * traffic is indistinguishable from the operator's own by origin alone.
1249
+ *
1250
+ * The share port refuses untokened requests by construction; the main port
1251
+ * keeps its existing behaviour unless the operator opts in with
1252
+ * `NEUROLINK_PROXY_REQUIRE_GRANT` — which stays the answer for anyone binding
1253
+ * `0.0.0.0` with nothing in front of it.
1254
+ */
1255
+ const isGatedListener = (c) => isGrantRequiredByEnv() || gatedShareRequests.has(c.req.raw);
1179
1256
  const readiness = createProxyReadinessState();
1180
1257
  const requestMetadata = new WeakMap();
1181
1258
  const recordRuntimeError = async (metadata, status, errorType, errorMessage, options) => {
@@ -1281,6 +1358,15 @@ export async function createProxyStartApp(params) {
1281
1358
  });
1282
1359
  });
1283
1360
  registerProxyRequestTracking(app, requestMetadata, readiness);
1361
+ // Complete-mode credentials adopted from a lender have to check in, or they
1362
+ // stop when their offline grace runs out. The timer is unref'd so it never
1363
+ // keeps the process alive, and failures are the lender being unreachable —
1364
+ // exactly the case the grace period exists for, not an error.
1365
+ const { heartbeatDueResidentGrants } = await import("../../proxy/residentGrants.js");
1366
+ const residentHeartbeatTimer = setInterval(() => {
1367
+ void heartbeatDueResidentGrants().catch(() => undefined);
1368
+ }, 5 * 60 * 1000);
1369
+ residentHeartbeatTimer.unref();
1284
1370
  const runtimeConfigStore = params.runtimeConfigStore;
1285
1371
  const runtimeConfigProvider = runtimeConfigStore
1286
1372
  ? () => runtimeConfigStore.getSnapshot()
@@ -1358,6 +1444,60 @@ export async function createProxyStartApp(params) {
1358
1444
  }
1359
1445
  const logModel = sanitizeForLog(String(model));
1360
1446
  logger.always(`[proxy] ${c.req.method} ${c.req.path} → model=${logModel} ${stream} tools=${toolCount}`);
1447
+ // Peer-sharing gate. Runs here rather than as middleware because the
1448
+ // model is only known after the body is parsed, and the model allowlist
1449
+ // is one of the gates. Requests with no share token take the `local`
1450
+ // branch untouched, which is every request on a node that shares nothing.
1451
+ // `Number()` answers 0 for `null`, `""` and `[]` — all finite, all wrong.
1452
+ // A zero reaches `estimateHoldCoins` and clamps up to its 256 floor
1453
+ // rather than the 4096 an absent value defaults to, so the hold opened
1454
+ // for the request comes out sixteen times too small. Only a real positive
1455
+ // number counts here; anything else is treated as absent, which is the
1456
+ // conservative direction because it holds more, not less.
1457
+ const rawMaxTokens = body?.max_tokens;
1458
+ const requestedMaxTokens = typeof rawMaxTokens === "number" &&
1459
+ Number.isFinite(rawMaxTokens) &&
1460
+ rawMaxTokens > 0
1461
+ ? rawMaxTokens
1462
+ : undefined;
1463
+ // The heartbeat surface authenticates itself against the grant's lease
1464
+ // secret. Running it through the request gate as well would spend the
1465
+ // grant's rate allowance and open a coin hold for a call that consumes no
1466
+ // capacity at all.
1467
+ const shareOutcome = c.req.path.startsWith("/peer/")
1468
+ ? { kind: "local" }
1469
+ : await admitInboundShareRequest({
1470
+ headers: Object.fromEntries(c.req.raw.headers.entries()),
1471
+ model: String(model),
1472
+ ...(requestedMaxTokens !== undefined
1473
+ ? { maxTokens: requestedMaxTokens }
1474
+ : {}),
1475
+ requireGrant: isGatedListener(c),
1476
+ });
1477
+ if (shareOutcome.kind === "refused") {
1478
+ const refusal = shareOutcome.response;
1479
+ for (const [key, value] of Object.entries(refusal.headers)) {
1480
+ c.header(key, value);
1481
+ }
1482
+ if (metadata) {
1483
+ metadata.terminalErrorType = `share_${refusal.body.error.type}`;
1484
+ }
1485
+ // Narrow to the literals the gate can actually produce; Hono's json()
1486
+ // wants a status literal and the alternative is a cast.
1487
+ const refusalStatus = refusal.status === 401 ? 401 : refusal.status === 403 ? 403 : 429;
1488
+ return c.json(refusal.body, refusalStatus);
1489
+ }
1490
+ const shareContext = shareOutcome.kind === "admitted" ? shareOutcome.context : undefined;
1491
+ if (shareOutcome.kind === "admitted") {
1492
+ if (metadata) {
1493
+ metadata.shareRelease = shareOutcome.release;
1494
+ }
1495
+ else {
1496
+ // No tracked metadata means nothing will call the completion hook;
1497
+ // release immediately rather than leaking the concurrency slot.
1498
+ shareOutcome.release();
1499
+ }
1500
+ }
1361
1501
  const ctx = {
1362
1502
  requestId: metadata?.requestId ?? crypto.randomUUID(),
1363
1503
  method: c.req.method,
@@ -1385,7 +1525,9 @@ export async function createProxyStartApp(params) {
1385
1525
  c.header(key, value);
1386
1526
  }
1387
1527
  };
1388
- const result = await route.handler(ctx);
1528
+ const result = shareContext
1529
+ ? await runWithShareContext(shareContext, () => route.handler(ctx))
1530
+ : await route.handler(ctx);
1389
1531
  if (result instanceof Response) {
1390
1532
  // Streaming responses own their headers; merge in anything the
1391
1533
  // handler published on the context that the Response lacks. A Response
@@ -1485,6 +1627,19 @@ export async function createProxyStartApp(params) {
1485
1627
  }));
1486
1628
  });
1487
1629
  app.get("/status", async (c) => {
1630
+ // A gated proxy is, by definition, one that may be exposed. `/status`
1631
+ // enumerates account labels — which for an OAuth account is an email — plus
1632
+ // quota and cooldown state, and it is not behind the request gate, so
1633
+ // without this anyone who reaches the tunnel could read the pool's identity.
1634
+ //
1635
+ // A loopback check would be no defence: cloudflared runs locally and
1636
+ // connects to 127.0.0.1, so tunnelled traffic arrives from loopback too.
1637
+ // Identity is therefore released only to a caller holding the update-control
1638
+ // token, and redacted for everyone else. Counters and health stay visible so
1639
+ // liveness tooling keeps working.
1640
+ const statusIdentityAllowed = !isGatedListener(c) ||
1641
+ c.req.header("x-neurolink-update-token") ===
1642
+ (params.updateControlToken ?? PROXY_UPDATE_CONTROL_TOKEN);
1488
1643
  const runtimeConfig = params.runtimeConfigStore?.getSnapshot();
1489
1644
  const runtimeConfigStatus = params.runtimeConfigStore?.getStatus();
1490
1645
  const activeStrategy = runtimeConfig
@@ -1749,8 +1904,10 @@ export async function createProxyStartApp(params) {
1749
1904
  terminalErrorDetailsMissing,
1750
1905
  terminalErrorDetailsExcess,
1751
1906
  snapshotSource,
1752
- accounts: accountRows,
1753
- primaryAccount,
1907
+ accounts: redactStatusAccounts(accountRows, statusIdentityAllowed),
1908
+ primaryAccount: statusIdentityAllowed
1909
+ ? primaryAccount
1910
+ : redactStatusPrimaryAccount(primaryAccount),
1754
1911
  persistence: getUsageStatsPersistenceStatus(),
1755
1912
  },
1756
1913
  activity: (() => {
@@ -2018,6 +2175,33 @@ function startProxyBackgroundMaintenance(logsDir, getAccountAllowlist) {
2018
2175
  }
2019
2176
  function registerProxyShutdownHandlers(params) {
2020
2177
  let shutdownStarted = false;
2178
+ /**
2179
+ * Await one shutdown step, but never for longer than `ms`.
2180
+ *
2181
+ * By the time a signal has arrived every step here is best-effort: the
2182
+ * process is going away either way, and the only thing an unbounded await
2183
+ * buys is the chance that the supervisor's patience runs out first and turns
2184
+ * a clean exit into a SIGKILL.
2185
+ */
2186
+ const withShutdownDeadline = async (step, ms, label) => {
2187
+ if (!step) {
2188
+ return;
2189
+ }
2190
+ let timer;
2191
+ const timedOut = await Promise.race([
2192
+ step.then(() => false, () => false),
2193
+ new Promise((resolve) => {
2194
+ timer = setTimeout(() => resolve(true), ms);
2195
+ timer.unref?.();
2196
+ }),
2197
+ ]);
2198
+ if (timer) {
2199
+ clearTimeout(timer);
2200
+ }
2201
+ if (timedOut) {
2202
+ logger.always(`[proxy] ${label} did not close in time; continuing`);
2203
+ }
2204
+ };
2021
2205
  const closeServer = async () => {
2022
2206
  const close = params.server.close?.bind(params.server);
2023
2207
  if (!close) {
@@ -2057,6 +2241,11 @@ function registerProxyShutdownHandlers(params) {
2057
2241
  await params.logCleanupScheduler.stop();
2058
2242
  params.updaterSupervisor?.stop();
2059
2243
  params.stopRuntimeConfig?.();
2244
+ // Bounded like the main server's close below. `stop()` awaits the
2245
+ // listener's own close, and a borrower holding a stream open keeps that
2246
+ // pending for as long as it likes — an unbounded await here is a proxy
2247
+ // that appears to ignore SIGTERM until the borrower hangs up.
2248
+ await withShutdownDeadline(params.shareListener?.stop(), SHARE_LISTENER_CLOSE_TIMEOUT_MS, "share listener");
2060
2249
  logger.always(`\nShutting down proxy (${signal})...`);
2061
2250
  let exitCode = signal === "SIGINT" || signal === "ROLLING_DRAIN" ? 0 : 1;
2062
2251
  if (!options?.skipServerClose) {
@@ -2140,6 +2329,66 @@ async function startProxyRuntime(params) {
2140
2329
  port: params.port,
2141
2330
  hostname: params.host,
2142
2331
  });
2332
+ // The gate-only listener. It exists only while this node lends something, and
2333
+ // it is the port an operator exposes: the main port keeps serving the
2334
+ // operator's own untokened client exactly as before.
2335
+ const { isShareListenerDisabled, superviseShareListener } = await import("../../proxy/shareListener.js");
2336
+ // Runs under socket workers too. During a rolling replacement both
2337
+ // generations are briefly live and the incoming one loses the bind; that is
2338
+ // now a logged retry rather than a crash, and the supervisor picks the port up
2339
+ // on its next poll once the outgoing worker drains. Disabling it here instead
2340
+ // would leave launchd installs — the main production shape — with no share
2341
+ // listener at all, which is the thing this exists to remove.
2342
+ const shareListener = isShareListenerDisabled()
2343
+ ? undefined
2344
+ : superviseShareListener({
2345
+ start: async () => {
2346
+ // Bind before reporting success, and take the bind error as a
2347
+ // rejection rather than an unhandled `error` event — a port
2348
+ // collision on the derived `port + 1` must cost the operator a log
2349
+ // line and a `--share-port`, never the whole proxy.
2350
+ const shareServer = await new Promise((resolve, reject) => {
2351
+ let listening = false;
2352
+ const started = serve({
2353
+ // Every request this listener accepts is gated, and this is
2354
+ // the only place that fact is known for certain — see
2355
+ // `gatedShareRequests`.
2356
+ fetch: (request, env) => {
2357
+ gatedShareRequests.add(request);
2358
+ return params.app.fetch(request, env);
2359
+ },
2360
+ port: params.sharePort,
2361
+ hostname: params.host,
2362
+ }, () => {
2363
+ listening = true;
2364
+ resolve(started);
2365
+ });
2366
+ started.on("error", (error) => {
2367
+ if (!listening) {
2368
+ reject(error);
2369
+ return;
2370
+ }
2371
+ // Past startup a listener error is a runtime event, not a
2372
+ // reason to take the process down with it.
2373
+ logger.always(`[proxy] share listener error: ${error.message}`);
2374
+ });
2375
+ });
2376
+ return {
2377
+ port: params.sharePort,
2378
+ close: () => new Promise((resolve) => {
2379
+ const close = shareServer.close?.bind(shareServer);
2380
+ if (!close) {
2381
+ resolve();
2382
+ return;
2383
+ }
2384
+ close(() => resolve());
2385
+ }),
2386
+ };
2387
+ },
2388
+ });
2389
+ // Bring it up now if grants already exist, rather than waiting a poll cycle
2390
+ // for a proxy that restarted with shares already issued.
2391
+ await shareListener?.poll();
2143
2392
  const managedByLaunchd = isLaunchdManagedProcess() || socketWorker;
2144
2393
  // launchd already owns restart supervision. A second detached supervisor can
2145
2394
  // outlive its parent and terminate a healthy replacement, so the guard is
@@ -2216,6 +2465,7 @@ async function startProxyRuntime(params) {
2216
2465
  pid: process.pid,
2217
2466
  port: params.port,
2218
2467
  host: params.host,
2468
+ sharePort: shareListener ? params.sharePort : undefined,
2219
2469
  strategy: activeStrategy,
2220
2470
  startTime: new Date().toISOString(),
2221
2471
  ready: true,
@@ -2355,6 +2605,7 @@ async function startProxyRuntime(params) {
2355
2605
  isDev,
2356
2606
  updaterSupervisor,
2357
2607
  stopRuntimeConfig,
2608
+ shareListener,
2358
2609
  registerSignals: !socketWorker,
2359
2610
  ...maintenance,
2360
2611
  });
@@ -2497,7 +2748,7 @@ async function startProxyCommandHandler(argv) {
2497
2748
  }
2498
2749
  // In dev mode: redirect writable state to .neurolink-dev/ and skip singleton check
2499
2750
  let devPaths;
2500
- const { resolveProxyPaths, resolveProxyUsageStatsPath } = await import("../../proxy/proxyPaths.js");
2751
+ const { resolveProxyPaths, resolveProxyUsageStatsPath, resolveProxyGrantsPath, resolveProxyLedgerPath, resolveProxyPeersPath, resolveProxyResidentGrantsPath, resolveProxyNotesPath, resolveProxyProvisioningPath, resolveProxyReceiptsPath, resolveProxyShareAuditPath, } = await import("../../proxy/proxyPaths.js");
2501
2752
  const proxyPaths = resolveProxyPaths(isDev);
2502
2753
  if (isDev) {
2503
2754
  devPaths = proxyPaths;
@@ -2506,6 +2757,22 @@ async function startProxyCommandHandler(argv) {
2506
2757
  initAccountQuota(devPaths.quotaFile);
2507
2758
  const { initAccountCooldown } = await import("../../proxy/accountCooldown.js");
2508
2759
  initAccountCooldown(devPaths.cooldownFile);
2760
+ const { initShareGrants } = await import("../../proxy/shareGrants.js");
2761
+ initShareGrants(resolveProxyGrantsPath(devPaths));
2762
+ const { initShareLedger } = await import("../../proxy/shareLedger.js");
2763
+ initShareLedger(resolveProxyLedgerPath(devPaths));
2764
+ const { initPeerStore } = await import("../../proxy/peerStore.js");
2765
+ initPeerStore(resolveProxyPeersPath(devPaths));
2766
+ const { initResidentGrants } = await import("../../proxy/residentGrants.js");
2767
+ initResidentGrants(resolveProxyResidentGrantsPath(devPaths));
2768
+ const { initShareAudit } = await import("../../proxy/shareAudit.js");
2769
+ initShareAudit(resolveProxyShareAuditPath(devPaths));
2770
+ const { initShareProvisioning } = await import("../../proxy/shareProvisioning.js");
2771
+ initShareProvisioning(resolveProxyProvisioningPath(devPaths));
2772
+ const { initShareReceipts } = await import("../../proxy/shareReceipts.js");
2773
+ initShareReceipts(resolveProxyReceiptsPath(devPaths));
2774
+ const { initShareNotes } = await import("../../proxy/shareNotes.js");
2775
+ initShareNotes(resolveProxyNotesPath(devPaths));
2509
2776
  // Ensure the dev state directory exists
2510
2777
  const { mkdirSync, existsSync } = await import("fs");
2511
2778
  if (!existsSync(devPaths.stateDir)) {
@@ -2562,6 +2829,11 @@ async function startProxyCommandHandler(argv) {
2562
2829
  spinner.text = "Configuring server...";
2563
2830
  }
2564
2831
  const port = argv.port ?? 55669;
2832
+ const { resolveSharePort } = await import("../../proxy/shareListener.js");
2833
+ const sharePort = resolveSharePort({
2834
+ ...(argv.sharePort !== undefined ? { explicit: argv.sharePort } : {}),
2835
+ mainPort: port,
2836
+ });
2565
2837
  const host = argv.host ?? "127.0.0.1";
2566
2838
  const { app, readiness } = await createProxyStartApp({
2567
2839
  neurolink,
@@ -2574,6 +2846,7 @@ async function startProxyCommandHandler(argv) {
2574
2846
  primaryAccountKey,
2575
2847
  accountAllowlist,
2576
2848
  runtimeConfigStore,
2849
+ sharePort,
2577
2850
  });
2578
2851
  await initializeProxyOpenTelemetry();
2579
2852
  if (spinner) {
@@ -2586,6 +2859,7 @@ async function startProxyCommandHandler(argv) {
2586
2859
  readiness,
2587
2860
  host,
2588
2861
  port,
2862
+ sharePort,
2589
2863
  strategy,
2590
2864
  proxyConfig,
2591
2865
  accountAllowlist,
@@ -2619,6 +2893,12 @@ export const proxyStartCommand = {
2619
2893
  alias: "p",
2620
2894
  default: 55669,
2621
2895
  description: "Port to listen on",
2896
+ })
2897
+ .option("share-port", {
2898
+ type: "number",
2899
+ alias: "sharePort",
2900
+ description: "Gate-only listener port for peer sharing (default: port + 1). " +
2901
+ "Runs only while at least one active grant exists; this is the port to expose.",
2622
2902
  })
2623
2903
  .option("host", {
2624
2904
  type: "string",
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `neurolink proxy expose` — put this node's proxy on a public URL.
3
+ *
4
+ * Wraps `cloudflared` because that is the shortest path from a laptop to a URL
5
+ * a peer can reach, with no port forwarding and no inbound firewall change.
6
+ *
7
+ * **The safety check is the point.** Whether a port is gated depends on the
8
+ * proxy process — which listener it is, and how that process was started —
9
+ * none of which this command can read. So instead of trusting configuration, it
10
+ * asks the running proxy directly: a request with no share token must be
11
+ * refused. A port that answers one is open, and exposing it would publish the
12
+ * operator's subscription to anyone who finds the URL, so the tunnel is refused
13
+ * rather than opened.
14
+ *
15
+ * With no `--port` it targets the gate-only share listener, which is the port
16
+ * that exists to face outward.
17
+ *
18
+ * @module cli/commands/proxyExpose
19
+ */
20
+ import type { CommandModule } from "yargs";
21
+ import type { ProxyExposeArgs, ProxyGateProbe } from "../../types/index.js";
22
+ /**
23
+ * Ask the running proxy whether it refuses untokened traffic.
24
+ *
25
+ * A refusal carrying `x-neurolink-grant-reason: missing_token` is proof the
26
+ * gate is live. Anything else — an answer, an upstream error, a credentials
27
+ * complaint — means the request got past the gate, which is the dangerous case.
28
+ *
29
+ * `scheme` matters: probing a TLS address over plain http fails to connect,
30
+ * which reads as unreachable, and an unreachable address is reported as *not*
31
+ * dangerous. A public `https://` URL would therefore never raise the warning it
32
+ * exists to raise.
33
+ */
34
+ export declare function probeProxyGate(host: string, port: number, scheme?: "http" | "https"): Promise<ProxyGateProbe>;
35
+ export declare const proxyExposeCommand: CommandModule<object, ProxyExposeArgs>;