@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
@@ -32,12 +32,26 @@ import { relocateClientSystemIntoMessages } from "../../proxy/systemRelocation.j
32
32
  import { logBodyCapture, logRequest, logRequestAttempt, } from "../../proxy/requestLogger.js";
33
33
  import { buildClientAttribution } from "../../proxy/clientAttribution.js";
34
34
  import { createSSEInterceptor } from "../../proxy/sseInterceptor.js";
35
+ import { selectBorrowablePeers } from "../../proxy/peerStore.js";
36
+ import { evaluateResidentAccount, getResidentGrantForAccount, recordResidentSpend, } from "../../proxy/residentGrants.js";
37
+ import { buildProvisionClaim, isLeaseRefusal, issueLease, } from "../../proxy/shareLease.js";
38
+ import { forwardToPeer } from "../../proxy/peerTransport.js";
39
+ import { getShareContext } from "../../proxy/shareContext.js";
40
+ import { buildShareRefusal, extractShareToken } from "../../proxy/shareGate.js";
41
+ import { debitShareGrantCoins, getNodePublicUrl, getNoteSecret, getShareGrant, resolveShareToken, setShareGrantState, } from "../../proxy/shareGrants.js";
42
+ import { recordAuditObservation } from "../../proxy/shareAudit.js";
43
+ import { claimProvisionRequest, openProvisionRequest, } from "../../proxy/shareProvisioning.js";
44
+ import { applyReciprocalNetting, listShareReceipts, } from "../../proxy/shareReceipts.js";
45
+ import { decodeShareNote, inspectShareNote, redeemShareNote, } from "../../proxy/shareNotes.js";
46
+ import { verifySharePayload } from "../../proxy/shareSigning.js";
47
+ import { availableCoins, readSharePoolWindowUsage, readShareWindowUsage, recordShareWindowDelta, settleShareUsage, usageToCoins, } from "../../proxy/shareLedger.js";
48
+ import { accountsInGrantScope, filterAccountsForGrant, isModelAllowed, isWithinSchedule, shareRefusalStatus, summarizeAccountExclusions, } from "../../proxy/sharePolicy.js";
35
49
  import { createStreamTerminalOutcomeTracker, mergeStreamTerminalOutcome, preflightAnthropicStream, } from "../../proxy/streamOutcome.js";
36
50
  import { isPermanentRefreshFailure, needsRefresh, persistTokens, refreshToken, refreshTokenFromLatest, } from "../../proxy/tokenRefresh.js";
37
51
  import { buildProxyTranslationPlan, parseRetryAfterMs, } from "../../proxy/routingPolicy.js";
38
52
  import { normalizeMaxInflightPerAccount } from "../../proxy/modelRouter.js";
39
53
  import { writeJsonSnapshotAtomically } from "../../proxy/snapshotPersistence.js";
40
- import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "../../proxy/usageStats.js";
54
+ import { getAccountStats, recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "../../proxy/usageStats.js";
41
55
  import { sanitizeForLog } from "../../utils/logSanitize.js";
42
56
  import { logger } from "../../utils/logger.js";
43
57
  import { raceWithAbort, withTimeout } from "../../utils/async/withTimeout.js";
@@ -482,7 +496,10 @@ function publishLimitHeaders(ctx, args) {
482
496
  ...(pool ? { pool } : {}),
483
497
  },
484
498
  });
485
- ctx.responseHeaders = { ...(ctx.responseHeaders ?? {}), ...headers };
499
+ ctx.responseHeaders = {
500
+ ...(ctx.responseHeaders ?? {}),
501
+ ...redactHeadersForBorrower(headers),
502
+ };
486
503
  }
487
504
  catch (error) {
488
505
  // Diagnostics must never break a response that is otherwise fine.
@@ -2457,6 +2474,540 @@ async function handleClaudePassthroughJsonResponse(args) {
2457
2474
  }
2458
2475
  return responseJson;
2459
2476
  }
2477
+ /**
2478
+ * Narrow the pool to what the current borrowed request's grant may use.
2479
+ *
2480
+ * A no-op — and one map lookup — for the node's own traffic, which is every
2481
+ * request on a proxy that shares nothing.
2482
+ *
2483
+ * When the filter empties the pool the refusal must be share-shaped, not the
2484
+ * generic "no credentials" 401: the borrower has perfectly good credentials,
2485
+ * the lender is simply holding capacity back. Saying it precisely is what lets
2486
+ * the borrower fall through to another peer instead of re-authenticating.
2487
+ */
2488
+ async function applyShareAccountGates(args) {
2489
+ const share = getShareContext();
2490
+ if (!share) {
2491
+ return { accounts: args.accounts };
2492
+ }
2493
+ if (args.accounts.length === 0) {
2494
+ // Nothing to withhold. Claiming a share refusal here would tell the
2495
+ // borrower the lender is holding capacity back, when in fact the lender has
2496
+ // no usable accounts at all — a different problem with a different fix,
2497
+ // and the existing no-credentials path already words it correctly.
2498
+ return { accounts: args.accounts };
2499
+ }
2500
+ const now = Date.now();
2501
+ const views = await Promise.all(args.accounts.map(async (account) => {
2502
+ const quota = getOrCreateRuntimeState(account.key).quota;
2503
+ const sessionResetAt = resetEpochToMs(quota?.sessionResetAt, now) ?? null;
2504
+ const weeklyResetAt = resetEpochToMs(quota?.weeklyResetAt, now) ?? null;
2505
+ const borrowed = await readShareWindowUsage(share.grantId, account.key, sessionResetAt, weeklyResetAt);
2506
+ return {
2507
+ accountKey: account.key,
2508
+ sessionUsed: quota?.sessionUsed ?? null,
2509
+ weeklyUsed: quota?.weeklyUsed ?? null,
2510
+ sessionResetAt,
2511
+ weeklyResetAt,
2512
+ borrowedSessionFraction: borrowed.sessionFraction,
2513
+ borrowedWeeklyFraction: borrowed.weeklyFraction,
2514
+ };
2515
+ }));
2516
+ const grant = await getShareGrant(share.grantId);
2517
+ if (!grant) {
2518
+ return { accounts: args.accounts };
2519
+ }
2520
+ // Pool-wide first: how much of the pool this grant has already taken. The
2521
+ // denominator is the accounts the grant may actually draw on — an
2522
+ // `--accounts`-restricted grant divides by those, not by every credential
2523
+ // this node holds, or its ceiling would scale with the size of a pool it
2524
+ // cannot reach.
2525
+ const poolUsage = await readSharePoolWindowUsage(share.grantId, accountsInGrantScope(grant.gates, views).inScope);
2526
+ const decision = filterAccountsForGrant(grant, views, now, poolUsage);
2527
+ const allowed = new Set(decision.allowed);
2528
+ const survivors = args.accounts.filter((account) => allowed.has(account.key));
2529
+ if (survivors.length > 0) {
2530
+ return { accounts: survivors };
2531
+ }
2532
+ const reason = summarizeAccountExclusions(decision.excluded);
2533
+ const refusal = buildShareRefusal(reason, {
2534
+ status: shareRefusalStatus(reason),
2535
+ grant,
2536
+ retryAfterSeconds: earliestShareRecoverySeconds(views, now),
2537
+ });
2538
+ // Assigned, not merged into an existing object: a refusal is often the first
2539
+ // thing to touch this context, and only copying when headers already existed
2540
+ // meant the borrower learned nothing about why it was refused.
2541
+ args.ctx.responseHeaders = {
2542
+ ...(args.ctx.responseHeaders ?? {}),
2543
+ ...refusal.headers,
2544
+ };
2545
+ logger.always(`[proxy] share ${share.peerLabel} withheld: ${reason} (${decision.excluded.length} accounts)`);
2546
+ return {
2547
+ accounts: [],
2548
+ refusal: {
2549
+ response: args.buildLoggedClaudeError(refusal.status, refusal.body.error.message, refusal.body.error.type),
2550
+ status: refusal.status,
2551
+ message: refusal.body.error.message,
2552
+ },
2553
+ };
2554
+ }
2555
+ /**
2556
+ * Withhold credentials a lender provisioned here whose lease has lapsed.
2557
+ *
2558
+ * A complete-mode credential keeps working while the lender is unreachable —
2559
+ * that is what it is for — but only for as long as the lease allows. Past that,
2560
+ * the honest thing is to stop using it, and the borrower is the only party in a
2561
+ * position to do so.
2562
+ *
2563
+ * A no-op for a node with no resident grants, which is every node until someone
2564
+ * provisions one.
2565
+ */
2566
+ async function dropExpiredResidentAccounts(accounts) {
2567
+ const survivors = [];
2568
+ const withheld = [];
2569
+ for (const account of accounts) {
2570
+ const verdict = await evaluateResidentAccount(account.key);
2571
+ if (!verdict) {
2572
+ survivors.push(account);
2573
+ continue;
2574
+ }
2575
+ if (!isLeaseRefusal(verdict)) {
2576
+ survivors.push(account);
2577
+ continue;
2578
+ }
2579
+ logger.always(`[proxy] withholding leased account=${account.label}: ${verdict.reason}`);
2580
+ withheld.push({ label: account.label, reason: verdict.reason });
2581
+ }
2582
+ return { accounts: survivors, withheld };
2583
+ }
2584
+ /**
2585
+ * Enforce a lender's model allowlist on the credential they provisioned here.
2586
+ *
2587
+ * A live share checks this at the lender's gate. A complete share has no gate in
2588
+ * the request path, so the borrower has to hold the line itself — otherwise a
2589
+ * grant that says "Sonnet only" becomes "anything" the moment it is provisioned,
2590
+ * which is exactly the property that would make complete mode unshippable.
2591
+ */
2592
+ async function dropLeaseDisallowedAccounts(accounts, requestedModel) {
2593
+ const survivors = [];
2594
+ const withheld = [];
2595
+ const now = Date.now();
2596
+ for (const account of accounts) {
2597
+ const resident = await getResidentGrantForAccount(account.key);
2598
+ if (!resident) {
2599
+ survivors.push(account);
2600
+ continue;
2601
+ }
2602
+ const gates = resident.lease.gates;
2603
+ // Request-level gates first: they do not depend on any window figure, and
2604
+ // saying "this share does not cover Opus" is more useful than saying the
2605
+ // slice is spent when both are true.
2606
+ if (!isModelAllowed(gates.models, requestedModel)) {
2607
+ logger.always(`[proxy] leased account=${account.label} does not cover model ${sanitizeForLog(requestedModel ?? "unknown")}`);
2608
+ withheld.push({ label: account.label, reason: "model_not_allowed" });
2609
+ continue;
2610
+ }
2611
+ if (gates.schedule && !isWithinSchedule(gates.schedule, now)) {
2612
+ logger.always(`[proxy] leased account=${account.label} is outside its allowed hours`);
2613
+ withheld.push({ label: account.label, reason: "out_of_window" });
2614
+ continue;
2615
+ }
2616
+ // Account-level gates through the lender's own evaluator, on the lender's
2617
+ // own numbers. A resident credential is minted from exactly one account, so
2618
+ // the pool of the pool-wide slice is that one account and the formula
2619
+ // collapses to the per-account case — which is precisely why the same code
2620
+ // can serve both sides.
2621
+ const quota = getOrCreateRuntimeState(account.key).quota;
2622
+ const sessionResetAt = resetEpochToMs(quota?.sessionResetAt, now) ?? null;
2623
+ const weeklyResetAt = resetEpochToMs(quota?.weeklyResetAt, now) ?? null;
2624
+ const borrowed = await readShareWindowUsage(resident.grantId, account.key, sessionResetAt, weeklyResetAt);
2625
+ const view = {
2626
+ accountKey: account.key,
2627
+ sessionUsed: quota?.sessionUsed ?? null,
2628
+ weeklyUsed: quota?.weeklyUsed ?? null,
2629
+ sessionResetAt,
2630
+ weeklyResetAt,
2631
+ borrowedSessionFraction: borrowed.sessionFraction,
2632
+ borrowedWeeklyFraction: borrowed.weeklyFraction,
2633
+ };
2634
+ const poolUsage = await readSharePoolWindowUsage(resident.grantId, [view]);
2635
+ // `gates.accounts` names the *lender's* accounts and means nothing here —
2636
+ // the credential this lease governs is the only account in scope by
2637
+ // construction. Carrying it over would filter the account out by its local
2638
+ // label and withhold every leased request.
2639
+ const { accounts: _lenderAccounts, ...localGates } = gates;
2640
+ const decision = filterAccountsForGrant(leaseAsGrant(resident, localGates), [view], now, poolUsage);
2641
+ if (decision.allowed.length === 0) {
2642
+ const reason = summarizeAccountExclusions(decision.excluded);
2643
+ logger.always(`[proxy] withholding leased account=${account.label}: ${reason}`);
2644
+ withheld.push({ label: account.label, reason });
2645
+ continue;
2646
+ }
2647
+ survivors.push(account);
2648
+ }
2649
+ return { accounts: survivors, withheld };
2650
+ }
2651
+ /**
2652
+ * Dress a lease up as the grant its gates came from.
2653
+ *
2654
+ * The borrower has no grant record — only the signed projection of one — but the
2655
+ * admission evaluator takes a grant. Rebuilding one here means both sides run
2656
+ * the identical gate code rather than a borrower-side reimplementation that
2657
+ * drifts from the lender's the first time a gate is added.
2658
+ */
2659
+ function leaseAsGrant(resident, gates) {
2660
+ const snapshot = resident.lease.entitlementSnapshot;
2661
+ return {
2662
+ schemaVersion: 1,
2663
+ id: resident.grantId,
2664
+ peerLabel: resident.lease.peerLabel,
2665
+ // Never compared: the lease's own signature is what authenticates it, and
2666
+ // it was checked before this account was allowed to reach here at all.
2667
+ tokenHash: "",
2668
+ tokenSalt: "",
2669
+ level: "complete",
2670
+ state: "active",
2671
+ entitlement: snapshot === "unlimited"
2672
+ ? { ledger: "unlimited" }
2673
+ : { ledger: "coins", coins: snapshot },
2674
+ gates,
2675
+ createdAt: resident.lease.issuedAt,
2676
+ updatedAt: resident.lease.issuedAt,
2677
+ };
2678
+ }
2679
+ /**
2680
+ * Attribute a window movement to the grant that caused it.
2681
+ *
2682
+ * A no-op for the node's own traffic. Fire-and-forget: bookkeeping must never
2683
+ * delay or fail a response the borrower is already receiving.
2684
+ */
2685
+ function recordBorrowedWindowDelta(accountKey, before, after) {
2686
+ const now = Date.now();
2687
+ const observe = (grantId) => {
2688
+ void recordShareWindowDelta({
2689
+ grantId,
2690
+ accountKey,
2691
+ sessionBefore: before?.sessionUsed ?? null,
2692
+ sessionAfter: after.sessionUsed,
2693
+ sessionResetAt: resetEpochToMs(after.sessionResetAt, now) ?? null,
2694
+ weeklyBefore: before?.weeklyUsed ?? null,
2695
+ weeklyAfter: after.weeklyUsed,
2696
+ weeklyResetAt: resetEpochToMs(after.weeklyResetAt, now) ?? null,
2697
+ });
2698
+ };
2699
+ const share = getShareContext();
2700
+ if (share) {
2701
+ observe(share.grantId);
2702
+ return;
2703
+ }
2704
+ // Not borrowed *from* us — but it may be borrowed *by* us. A leased account's
2705
+ // windows move because this node spent them, and the lease's slice ceiling has
2706
+ // nothing to measure unless that movement is recorded against the grant. The
2707
+ // lender keeps the same book on its side; both read it with the same formula.
2708
+ void getResidentGrantForAccount(accountKey)
2709
+ .then((resident) => {
2710
+ if (resident) {
2711
+ observe(resident.grantId);
2712
+ }
2713
+ })
2714
+ .catch(() => {
2715
+ // Bookkeeping only — never fail a response the client already has.
2716
+ });
2717
+ }
2718
+ /**
2719
+ * Charge a completed borrowed request against its grant.
2720
+ *
2721
+ * A no-op for the node's own traffic, and fire-and-forget for the same reason
2722
+ * as the window delta above.
2723
+ */
2724
+ /**
2725
+ * Bill a request served by a credential a lender provisioned here.
2726
+ *
2727
+ * The mirror of `settleBorrowedRequest`: that one charges a borrower on the
2728
+ * lender's node, this one records what *this* node owes a lender whose
2729
+ * credential it is holding. Without it a complete-mode heartbeat reports zero
2730
+ * forever and the lender's balance never moves.
2731
+ */
2732
+ function recordLeasedAccountSpend(accountLabel, model, usage) {
2733
+ const coins = usageToCoins({
2734
+ inputTokens: usage.inputTokens ?? 0,
2735
+ outputTokens: usage.outputTokens ?? 0,
2736
+ ...(usage.cacheCreationTokens !== undefined
2737
+ ? { cacheCreationTokens: usage.cacheCreationTokens }
2738
+ : {}),
2739
+ ...(usage.cacheReadTokens !== undefined
2740
+ ? { cacheReadTokens: usage.cacheReadTokens }
2741
+ : {}),
2742
+ }, model);
2743
+ void recordResidentSpend(accountLabel, coins).catch(() => {
2744
+ // Bookkeeping only — never fail a response the client already has.
2745
+ });
2746
+ }
2747
+ /**
2748
+ * Charge a served non-streaming response to whichever ledger owns the account.
2749
+ *
2750
+ * Both sides are no-ops unless the account is actually borrowed or leased, so
2751
+ * this is safe to call on every response — which is the point: gating it on
2752
+ * anything else is how it came to be skipped.
2753
+ */
2754
+ function settleFromResponseUsage(account, responseJson) {
2755
+ if (!responseJson || typeof responseJson !== "object") {
2756
+ return;
2757
+ }
2758
+ const usage = responseJson.usage;
2759
+ if (!usage) {
2760
+ return;
2761
+ }
2762
+ const model = servedModelName(responseJson);
2763
+ const tokens = {
2764
+ inputTokens: usage.input_tokens ?? 0,
2765
+ outputTokens: usage.output_tokens ?? 0,
2766
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
2767
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0,
2768
+ };
2769
+ settleBorrowedRequest(account.key, model, tokens);
2770
+ recordLeasedAccountSpend(account.label, model, tokens);
2771
+ }
2772
+ function settleBorrowedRequest(accountKey, model, usage) {
2773
+ const share = getShareContext();
2774
+ if (!share) {
2775
+ return;
2776
+ }
2777
+ void settleShareUsage({
2778
+ grantId: share.grantId,
2779
+ accountKey,
2780
+ model: model ?? share.model,
2781
+ usage: {
2782
+ inputTokens: usage.inputTokens ?? 0,
2783
+ outputTokens: usage.outputTokens ?? 0,
2784
+ ...(usage.cacheCreationTokens !== undefined
2785
+ ? { cacheCreationTokens: usage.cacheCreationTokens }
2786
+ : {}),
2787
+ ...(usage.cacheReadTokens !== undefined
2788
+ ? { cacheReadTokens: usage.cacheReadTokens }
2789
+ : {}),
2790
+ },
2791
+ ...(share.holdId ? { holdId: share.holdId } : {}),
2792
+ });
2793
+ }
2794
+ /**
2795
+ * Remove what a borrower has no business seeing.
2796
+ *
2797
+ * `x-neurolink-account` carries the lender's account label, which for an OAuth
2798
+ * account is their email address; the pool counters describe the shape of a
2799
+ * pool that is not the borrower's. The borrower's own routing needs the quota
2800
+ * and grant headers, and nothing else here.
2801
+ *
2802
+ * A no-op for the node's own traffic, where these headers are exactly the
2803
+ * diagnostics the operator wants.
2804
+ */
2805
+ function redactHeadersForBorrower(headers) {
2806
+ if (!getShareContext()) {
2807
+ return headers;
2808
+ }
2809
+ const redacted = {};
2810
+ for (const [key, value] of Object.entries(headers)) {
2811
+ const lower = key.toLowerCase();
2812
+ if (lower === "x-neurolink-account" ||
2813
+ lower === "x-neurolink-account-type") {
2814
+ continue;
2815
+ }
2816
+ if (lower.startsWith("x-neurolink-pool-")) {
2817
+ continue;
2818
+ }
2819
+ redacted[key] = value;
2820
+ }
2821
+ return redacted;
2822
+ }
2823
+ /**
2824
+ * Record a heartbeat against the account's real utilization, and pause the grant
2825
+ * when the two have disagreed too many times in a row.
2826
+ *
2827
+ * The lender's own request count on that account is read from its usage stats:
2828
+ * an interval this node also used is not evidence about anybody, so the audit
2829
+ * abstains rather than guessing.
2830
+ */
2831
+ async function auditCompleteShareHeartbeat(grant, reportedCoins) {
2832
+ const accountLabel = grant.provisionedAccount;
2833
+ if (!accountLabel) {
2834
+ // Nothing to audit against: the grant predates provisioning or was attached
2835
+ // by hand. Say nothing rather than inventing a baseline.
2836
+ return { paused: false, detail: "no provisioned account recorded" };
2837
+ }
2838
+ const state = accountRuntimeState.get(`anthropic:${accountLabel}`);
2839
+ const stats = getAccountStats(accountLabel);
2840
+ const { verdict, shouldPause } = await recordAuditObservation({
2841
+ grantId: grant.id,
2842
+ accountLabel,
2843
+ lenderRequestsTotal: stats?.successCount ?? 0,
2844
+ observation: {
2845
+ at: Date.now(),
2846
+ sessionUsed: state?.quota?.sessionUsed ?? null,
2847
+ weeklyUsed: state?.quota?.weeklyUsed ?? null,
2848
+ reportedCoins,
2849
+ // Replaced with the per-interval delta by the recorder.
2850
+ lenderRequests: 0,
2851
+ },
2852
+ });
2853
+ if (shouldPause) {
2854
+ await setShareGrantState(grant.id, "paused");
2855
+ return {
2856
+ paused: true,
2857
+ detail: verdict.drifted ? verdict.detail : "repeated unexplained usage",
2858
+ };
2859
+ }
2860
+ return {
2861
+ paused: false,
2862
+ detail: verdict.drifted ? verdict.detail : "consistent",
2863
+ };
2864
+ }
2865
+ /**
2866
+ * Apply a complete-mode borrower's self-reported spend to their balance.
2867
+ *
2868
+ * Deliberately trusting at this layer and deliberately verified elsewhere: the
2869
+ * lender cannot see a resident credential's traffic directly, but it can see the
2870
+ * account's true utilization through the usage API, so under-reporting shows up
2871
+ * as drift rather than as free capacity.
2872
+ */
2873
+ async function recordReportedResidentSpend(grant, coins) {
2874
+ await debitShareGrantCoins(grant.id, coins);
2875
+ }
2876
+ /**
2877
+ * Compare a presented secret against the stored one without leaking where they
2878
+ * diverge. Hand-rolled rather than `crypto.timingSafeEqual` for the same reason
2879
+ * as `shareGrants.digestsMatch`: this module is reachable from a build whose
2880
+ * `node:crypto` stub does not carry it.
2881
+ */
2882
+ function secretsMatch(expected, presented) {
2883
+ if (expected.length !== presented.length || expected.length === 0) {
2884
+ return false;
2885
+ }
2886
+ let difference = 0;
2887
+ for (let index = 0; index < expected.length; index += 1) {
2888
+ difference |= expected.charCodeAt(index) ^ presented.charCodeAt(index);
2889
+ }
2890
+ return difference === 0;
2891
+ }
2892
+ /**
2893
+ * Peer wire protocol version. Bumped when a borrower would misread an older
2894
+ * node's answers — not when a field is added, which every version tolerates.
2895
+ */
2896
+ const PEER_PROTOCOL_VERSION = 1;
2897
+ /** What this node can do for a peer, so a borrower need not probe to find out. */
2898
+ const PEER_CAPABILITIES = ["live", "complete", "handshake", "limits"];
2899
+ /**
2900
+ * What a borrower may know about the lender's pool.
2901
+ *
2902
+ * Deliberately shaped as "what is left for *you*", never "what the lender has":
2903
+ * no labels, no per-account figures, no counts that would let a borrower infer
2904
+ * how many credentials sit behind the tunnel. Enough to route on and nothing
2905
+ * more, which is what `/peer/limits` is for.
2906
+ */
2907
+ async function buildPeerLimitsSnapshot(grant, allowlist) {
2908
+ const now = Date.now();
2909
+ const accounts = await listAnthropicAccountsForUsage(allowlist);
2910
+ const views = await Promise.all(accounts.map(async (account) => {
2911
+ const quota = getOrCreateRuntimeState(account.key).quota;
2912
+ const sessionResetAt = resetEpochToMs(quota?.sessionResetAt, now) ?? null;
2913
+ const weeklyResetAt = resetEpochToMs(quota?.weeklyResetAt, now) ?? null;
2914
+ const borrowed = await readShareWindowUsage(grant.id, account.key, sessionResetAt, weeklyResetAt);
2915
+ return {
2916
+ accountKey: account.key,
2917
+ sessionUsed: quota?.sessionUsed ?? null,
2918
+ weeklyUsed: quota?.weeklyUsed ?? null,
2919
+ sessionResetAt,
2920
+ weeklyResetAt,
2921
+ borrowedSessionFraction: borrowed.sessionFraction,
2922
+ borrowedWeeklyFraction: borrowed.weeklyFraction,
2923
+ };
2924
+ }));
2925
+ const inScope = accountsInGrantScope(grant.gates, views).inScope;
2926
+ const poolUsage = await readSharePoolWindowUsage(grant.id, inScope);
2927
+ const decision = filterAccountsForGrant(grant, views, now, poolUsage);
2928
+ const left = (ceiling, taken) => ceiling === undefined ? null : Math.max(0, ceiling - taken * 100);
2929
+ return {
2930
+ grantState: grant.state,
2931
+ level: grant.level,
2932
+ ledger: grant.entitlement.ledger,
2933
+ ...(grant.entitlement.ledger === "coins"
2934
+ ? { remainingCoins: Math.max(0, Math.floor(availableCoins(grant))) }
2935
+ : {}),
2936
+ servable: decision.allowed.length > 0,
2937
+ ...(decision.allowed.length === 0 && decision.excluded.length > 0
2938
+ ? { withheldReason: summarizeAccountExclusions(decision.excluded) }
2939
+ : {}),
2940
+ sliceLeftPct: {
2941
+ session: left(grant.gates.maxSlice?.session5hPct, poolUsage.sessionFraction),
2942
+ weekly: left(grant.gates.maxSlice?.weekly7dPct, poolUsage.weeklyFraction),
2943
+ },
2944
+ ...(decision.allowed.length === 0
2945
+ ? (() => {
2946
+ const retry = earliestShareRecoverySeconds(views, now);
2947
+ return retry === undefined ? {} : { retryAfterSeconds: retry };
2948
+ })()
2949
+ : {}),
2950
+ };
2951
+ }
2952
+ /**
2953
+ * Resolve the share token on a `/peer/*` call.
2954
+ *
2955
+ * These routes sit outside the request gate — they consume no capacity, and
2956
+ * running them through it would spend the grant's rate allowance on a call that
2957
+ * exists to ask whether spending is possible — so each one authenticates itself.
2958
+ */
2959
+ /**
2960
+ * Narrow a peer-auth outcome to its refusing half.
2961
+ *
2962
+ * Explicit rather than `!auth.ok`, because one of the package's build steps
2963
+ * compiles this file without `strictNullChecks`, where TypeScript will not
2964
+ * narrow a boolean discriminant at all — the same reason `isShareRefusal` and
2965
+ * `isLeaseRefusal` exist.
2966
+ */
2967
+ function isPeerAuthRefusal(outcome) {
2968
+ return !outcome.ok;
2969
+ }
2970
+ async function authenticatePeerRequest(headers) {
2971
+ const token = extractShareToken(headers);
2972
+ if (!token) {
2973
+ return {
2974
+ ok: false,
2975
+ body: buildShareRefusal("missing_token", { status: 401 }).body,
2976
+ };
2977
+ }
2978
+ const grant = await resolveShareToken(token);
2979
+ if (!grant) {
2980
+ return {
2981
+ ok: false,
2982
+ body: buildShareRefusal("unknown_token", { status: 401 }).body,
2983
+ };
2984
+ }
2985
+ return { ok: true, grant };
2986
+ }
2987
+ /** The model an upstream JSON response says it served, when it says so. */
2988
+ function servedModelName(payload) {
2989
+ if (!payload || typeof payload !== "object") {
2990
+ return undefined;
2991
+ }
2992
+ const model = payload.model;
2993
+ return typeof model === "string" ? model : undefined;
2994
+ }
2995
+ /**
2996
+ * Soonest window reset across the pool, in seconds.
2997
+ *
2998
+ * A withheld borrower's honest answer to "when should I come back" is when the
2999
+ * lender's tightest window turns over — anything sooner is a guess that invites
3000
+ * a retry storm.
3001
+ */
3002
+ function earliestShareRecoverySeconds(views, now) {
3003
+ const resets = views
3004
+ .flatMap((view) => [view.sessionResetAt, view.weeklyResetAt])
3005
+ .filter((value) => value !== null && value > now);
3006
+ if (resets.length === 0) {
3007
+ return undefined;
3008
+ }
3009
+ return Math.max(1, Math.round((Math.min(...resets) - now) / 1000));
3010
+ }
2460
3011
  async function loadClaudeProxyAccounts(args) {
2461
3012
  const { ctx, body, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, setRoutingDecision, } = args;
2462
3013
  const fs = await import("fs");
@@ -2646,9 +3197,54 @@ async function loadClaudeProxyAccounts(args) {
2646
3197
  reconcileEligibleAccountRuntimeState(account);
2647
3198
  }
2648
3199
  await seedRuntimeQuotasFromDisk(accounts);
2649
- const enabledAccounts = accounts.filter((account) => {
3200
+ const eligibleAccounts = accounts.filter((account) => {
2650
3201
  return !getOrCreateRuntimeState(account.key).permanentlyDisabled;
2651
3202
  });
3203
+ // Peer-sharing account gates. A borrowed request may only draw on accounts
3204
+ // its grant allows, that still leave the lender's reserved headroom intact,
3205
+ // and whose window slice the grant has not already spent. These are account
3206
+ // properties rather than request properties, which is why they filter the
3207
+ // pool here instead of refusing at the inbound gate.
3208
+ const leaseExpiryFiltered = await dropExpiredResidentAccounts(eligibleAccounts);
3209
+ const leaseScopeFiltered = await dropLeaseDisallowedAccounts(leaseExpiryFiltered.accounts, typeof body.model === "string" ? body.model : undefined);
3210
+ const leaseFiltered = {
3211
+ accounts: leaseScopeFiltered.accounts,
3212
+ withheld: [...leaseExpiryFiltered.withheld, ...leaseScopeFiltered.withheld],
3213
+ };
3214
+ const leasedAccounts = leaseFiltered.accounts;
3215
+ if (leasedAccounts.length === 0 && leaseFiltered.withheld.length > 0) {
3216
+ // Every account here belongs to a lender whose lease has lapsed. The
3217
+ // re-authentication message below would be actively wrong — it would send
3218
+ // the borrower to OAuth into somebody else's account, which cannot work and
3219
+ // should not be attempted. Say what actually happened instead.
3220
+ const detail = leaseFiltered.withheld
3221
+ .map((entry) => `${entry.label} (${entry.reason})`)
3222
+ .join(", ");
3223
+ // Scope and lifetime need different advice. Telling someone to re-sync when
3224
+ // the lender simply never lent them this model sends them in circles.
3225
+ const scopeOnly = leaseFiltered.withheld.every((entry) => entry.reason === "model_not_allowed");
3226
+ const leaseMessage = scopeOnly
3227
+ ? `Borrowed account(s) do not cover the requested model: ${detail}. ` +
3228
+ `Ask the lender to widen the share, or use a model it allows.`
3229
+ : `Borrowed account(s) are no longer covered by a lease: ${detail}. ` +
3230
+ `Run 'neurolink proxy peer sync' to check in with the lender, or ask them to resume the share.`;
3231
+ tracer?.setError("permission_error", leaseMessage);
3232
+ tracer?.end(403, Date.now() - requestStartTime);
3233
+ return {
3234
+ response: buildLoggedClaudeError(403, leaseMessage, "permission_error"),
3235
+ };
3236
+ }
3237
+ const shareFiltered = await applyShareAccountGates({
3238
+ accounts: leasedAccounts,
3239
+ ctx,
3240
+ buildLoggedClaudeError,
3241
+ });
3242
+ if (shareFiltered.refusal) {
3243
+ tracer?.setError("rate_limit_error", shareFiltered.refusal.message);
3244
+ tracer?.end(shareFiltered.refusal.status, Date.now() - requestStartTime);
3245
+ return { response: shareFiltered.refusal.response };
3246
+ }
3247
+ const enabledAccounts = shareFiltered.accounts;
2652
3248
  if (enabledAccounts.length === 0) {
2653
3249
  const reauthMsg = formatReauthMessage(accounts.map((account) => account.label));
2654
3250
  tracer?.setError("authentication_error", reauthMsg);
@@ -2834,6 +3430,62 @@ async function executeClaudeFallbackWithRetry(args) {
2834
3430
  }
2835
3431
  throw lastError;
2836
3432
  }
3433
+ /**
3434
+ * Try each borrowable peer in priority order once the local pool is spent.
3435
+ *
3436
+ * Returns a `Response` for a stream — which must keep streaming — the parsed
3437
+ * JSON body otherwise, or `null` when no peer served, leaving the provider
3438
+ * fallback chain to take over. The two success shapes are what the rest of this
3439
+ * module returns as well; the route adapter tells them apart.
3440
+ *
3441
+ * Deliberately one attempt per peer: this path only runs after every local
3442
+ * account has already been tried, so the request has spent most of its latency
3443
+ * budget. Re-trying a peer that just declined would spend the rest of it.
3444
+ *
3445
+ * A borrowed request is never itself forwarded to a peer. Chaining a lend onto
3446
+ * a lend would spend a third party's capacity under a grant that says nothing
3447
+ * about them, and a cycle between two nodes would bounce a single request
3448
+ * between them until something timed out.
3449
+ */
3450
+ async function tryBorrowFromPeers(args) {
3451
+ if (getShareContext()) {
3452
+ return null;
3453
+ }
3454
+ const peers = await selectBorrowablePeers();
3455
+ if (peers.length === 0) {
3456
+ return null;
3457
+ }
3458
+ const stream = args.body.stream === true;
3459
+ // Re-serialize the request as the client shaped it. The peer runs its own
3460
+ // routing and its own pool, so forwarding our resolved account or attempt
3461
+ // state would mean nothing to it.
3462
+ const forwardBody = JSON.stringify(args.body);
3463
+ for (const peer of peers) {
3464
+ logger.always(`[proxy] local pool spent — trying peer=${peer.name}`);
3465
+ const attempt = await forwardToPeer({ peer, body: forwardBody, stream });
3466
+ if (attempt.ok) {
3467
+ logger.always(`[proxy] served by peer=${peer.name}`);
3468
+ args.logFinalRequest(attempt.response.status, `peer:${peer.name}`, "peer");
3469
+ // Hand back what the rest of this module hands back: a locally
3470
+ // constructed Response for a stream, a parsed object for JSON. Returning
3471
+ // the fetch Response itself would be serialized to `{}` by the route
3472
+ // adapter, which only recognizes Responses this process created.
3473
+ if (stream) {
3474
+ return new Response(attempt.response.body, {
3475
+ status: attempt.response.status,
3476
+ headers: {
3477
+ "content-type": attempt.response.headers.get("content-type") ??
3478
+ "text/event-stream",
3479
+ "cache-control": "no-cache",
3480
+ connection: "keep-alive",
3481
+ },
3482
+ });
3483
+ }
3484
+ return (await attempt.response.json().catch(() => null)) ?? null;
3485
+ }
3486
+ }
3487
+ return null;
3488
+ }
2837
3489
  async function tryConfiguredClaudeFallbackChain(args) {
2838
3490
  const { ctx, body, parsedFallbackRequest, modelRouter, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
2839
3491
  const chain = modelRouter?.getFallbackChain() ?? [];
@@ -3232,6 +3884,10 @@ async function handleAnthropicSuccessfulResponse(args) {
3232
3884
  logger.always(`[proxy] ← ${response.status} account=${account.label}`);
3233
3885
  const quota = parseQuotaHeaders(response.headers, { model: body.model });
3234
3886
  if (quota) {
3887
+ // Attribute the window movement to the borrowing grant before the snapshot
3888
+ // is overwritten — this is the only moment both the previous and the new
3889
+ // utilization are in hand, and a slice ceiling is meaningless without it.
3890
+ recordBorrowedWindowDelta(account.key, accountState.quota, quota);
3235
3891
  // Stash the latest quota on runtime state so the next request can pick the
3236
3892
  // account whose window resets soonest (max-utilization) and proactively
3237
3893
  // skip rejected windows unless Anthropic explicitly permits overage.
@@ -3566,6 +4222,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
3566
4222
  const capturedResponse = response;
3567
4223
  const capturedRequestBytes = finalBodyStr.length;
3568
4224
  const capturedAccountLabel = account.label;
4225
+ const capturedAccountKey = account.key;
3569
4226
  telemetryDone = Promise.all([telemetry, clientCapture, streamOutcome])
3570
4227
  .then(([data, clientBody, rawOutcome]) => {
3571
4228
  const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
@@ -3576,6 +4233,20 @@ function attachAnthropicSuccessStreamTelemetry(args) {
3576
4233
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
3577
4234
  cacheReadTokens: data.usage.cacheReadInputTokens,
3578
4235
  });
4236
+ // Bill the borrowing grant from the same totals. A stream's usage is
4237
+ // only final at message_delta, which is exactly here.
4238
+ settleBorrowedRequest(capturedAccountKey, data.model, {
4239
+ inputTokens: data.usage.inputTokens,
4240
+ outputTokens: data.usage.outputTokens,
4241
+ cacheCreationTokens: data.usage.cacheCreationInputTokens,
4242
+ cacheReadTokens: data.usage.cacheReadInputTokens,
4243
+ });
4244
+ recordLeasedAccountSpend(capturedAccountLabel, data.model, {
4245
+ inputTokens: data.usage.inputTokens,
4246
+ outputTokens: data.usage.outputTokens,
4247
+ cacheCreationTokens: data.usage.cacheCreationInputTokens,
4248
+ cacheReadTokens: data.usage.cacheReadInputTokens,
4249
+ });
3579
4250
  capturedTracer.logStreamEvents(data.events);
3580
4251
  capturedTracer.setResponseInfo(responseInfoFromStream(data));
3581
4252
  const rateLimit5h = parseFloat(capturedResponse.headers.get("anthropic-ratelimit-unified-5h-utilization") ?? "");
@@ -3676,6 +4347,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
3676
4347
  });
3677
4348
  streamSource = streamSource.pipeThrough(noTracerInterceptor);
3678
4349
  const capturedAccountLabel = account.label;
4350
+ const capturedAccountKey = account.key;
3679
4351
  telemetryDone = Promise.all([
3680
4352
  noTracerTelemetry,
3681
4353
  clientCapture,
@@ -3691,6 +4363,10 @@ function attachAnthropicSuccessStreamTelemetry(args) {
3691
4363
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
3692
4364
  cacheReadTokens: data.usage.cacheReadInputTokens,
3693
4365
  };
4366
+ // Settled on the untraced path too: whether telemetry is exported has
4367
+ // nothing to do with whether a borrower should be charged.
4368
+ settleBorrowedRequest(capturedAccountKey, data.model, usage);
4369
+ recordLeasedAccountSpend(capturedAccountLabel, data.model, usage);
3694
4370
  if (failure) {
3695
4371
  logFinalRequest(failure.status, capturedAccountLabel, account.type, failure.errorType, failure.message, usage);
3696
4372
  }
@@ -3813,6 +4489,10 @@ async function handleAnthropicJsonSuccessResponse(args) {
3813
4489
  durationMs: Date.now() - requestStartTime,
3814
4490
  });
3815
4491
  const responseJson = JSON.parse(responseText);
4492
+ // Settlement is not diagnostics. It ran inside the tracer branch, so a node
4493
+ // with tracing off served every borrowed request for free and the lender's
4494
+ // ledger never moved — settle from the response itself instead.
4495
+ settleFromResponseUsage(account, responseJson);
3816
4496
  if (tracer && responseJson && typeof responseJson === "object") {
3817
4497
  const usage = responseJson.usage;
3818
4498
  if (usage) {
@@ -3928,6 +4608,9 @@ async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
3928
4608
  durationMs: Date.now() - requestStartTime,
3929
4609
  });
3930
4610
  const retryJson = JSON.parse(retryText);
4611
+ // A response served after an auth retry is a served response: it costs the
4612
+ // lender's account exactly what any other one does.
4613
+ settleFromResponseUsage(account, retryJson);
3931
4614
  if (tracer && retryJson && typeof retryJson === "object") {
3932
4615
  const retryUsage = retryJson.usage;
3933
4616
  if (retryUsage) {
@@ -5197,6 +5880,14 @@ async function handleAnthropicRoutedClaudeRequest(args) {
5197
5880
  setRoutingDecision,
5198
5881
  });
5199
5882
  if ("response" in loadedAccounts) {
5883
+ // No usable local account. A node that has none of its own — or whose only
5884
+ // accounts are disabled — is still entitled to borrow: that is the whole
5885
+ // point of being lent capacity. Peers are tried before the credentials
5886
+ // error is returned, and the error stands if none of them serves.
5887
+ const peerOnlyResult = await tryBorrowFromPeers({ body, logFinalRequest });
5888
+ if (peerOnlyResult) {
5889
+ return peerOnlyResult;
5890
+ }
5200
5891
  return loadedAccounts.response;
5201
5892
  }
5202
5893
  const { accounts, enabledAccounts, orderedAccounts, bodyStr, requestStart, toolCount, url, clientHeaders, isClaudeClientRequest, } = loadedAccounts;
@@ -5646,6 +6337,13 @@ async function handleAnthropicRoutedClaudeRequest(args) {
5646
6337
  // rather than delaying it or returning unrelated fallback output.
5647
6338
  if (shouldAttemptClaudeFallback(loopState)) {
5648
6339
  let fallbackFailureMessage;
6340
+ // Peers first. A peer serves the same models over the same wire format, so
6341
+ // borrowing costs one extra hop, while the provider chain below has to
6342
+ // reshape the request for a different API and answers as a different model.
6343
+ const peerResult = await tryBorrowFromPeers({ body, logFinalRequest });
6344
+ if (peerResult) {
6345
+ return peerResult;
6346
+ }
5649
6347
  const configuredFallbackResult = await tryConfiguredClaudeFallbackChain({
5650
6348
  ctx,
5651
6349
  body,
@@ -5948,6 +6646,358 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
5948
6646
  streaming: { enabled: true, contentType: "text/event-stream" },
5949
6647
  },
5950
6648
  // =====================================================================
6649
+ // POST /peer/heartbeat -- complete-mode check-in
6650
+ //
6651
+ // The only control surface a complete-mode borrower still touches. It
6652
+ // answers with a fresh lease while the grant is active, and with a stop
6653
+ // once it is not — which is how a pause reaches a borrower whose requests
6654
+ // never come through here at all.
6655
+ // =====================================================================
6656
+ {
6657
+ method: "POST",
6658
+ path: `${basePath}/peer/heartbeat`,
6659
+ handler: async (ctx) => {
6660
+ const token = ctx.headers["x-neurolink-share-token"];
6661
+ const grantId = ctx.headers["x-neurolink-grant-id"];
6662
+ if (!token || !grantId) {
6663
+ // A stop, not an HTTP error: the borrower reads `ok`/`stop` from
6664
+ // the body, and a status code here would be discarded by the route
6665
+ // adaptor while reading as though it were enforced.
6666
+ return { ok: false, stop: true, reason: "no grant identified" };
6667
+ }
6668
+ const grant = await getShareGrant(grantId);
6669
+ // The lease secret is the shared credential for this surface: the
6670
+ // borrower proves identity with the same secret it verifies leases
6671
+ // with, so a heartbeat needs no separate token to leak.
6672
+ if (!grant ||
6673
+ !grant.leaseSecret ||
6674
+ !secretsMatch(grant.leaseSecret, token)) {
6675
+ return { ok: false, stop: true, reason: "grant not recognized" };
6676
+ }
6677
+ if (grant.level !== "complete") {
6678
+ return {
6679
+ ok: false,
6680
+ stop: true,
6681
+ reason: "grant is not a complete share",
6682
+ };
6683
+ }
6684
+ if (grant.state !== "active") {
6685
+ return { ok: false, stop: true, reason: grant.state };
6686
+ }
6687
+ // Fold in what the borrower says it spent. This is self-reported and
6688
+ // treated as such — `share status` reconciles it against the
6689
+ // account's real utilization, which the borrower cannot influence.
6690
+ const body = ctx.body;
6691
+ const reported = Number(body?.coinsSpent ?? 0);
6692
+ if (Number.isFinite(reported) && reported > 0) {
6693
+ await recordReportedResidentSpend(grant, reported);
6694
+ }
6695
+ // Weigh the claim against what the account actually did. A borrower
6696
+ // that stops reporting is invisible in every other signal we have.
6697
+ const drift = await auditCompleteShareHeartbeat(grant, Number.isFinite(reported) ? reported : 0);
6698
+ if (drift.paused) {
6699
+ logger.always(`[proxy] auto-paused share ${grant.peerLabel}: ${drift.detail}`);
6700
+ return { ok: false, stop: true, reason: "usage drift" };
6701
+ }
6702
+ return { ok: true, lease: issueLease(grant) };
6703
+ },
6704
+ description: "Complete-share heartbeat: report spend, renew the lease",
6705
+ tags: ["claude-proxy", "sharing"],
6706
+ },
6707
+ // =====================================================================
6708
+ // GET /peer/handshake -- version and capability negotiation
6709
+ //
6710
+ // The cheapest possible "are we still on speaking terms": it touches no
6711
+ // account, spends no capacity and reaches no upstream, so a borrower can
6712
+ // call it on a timer. It reports the grant's own state and nothing about
6713
+ // the pool behind it.
6714
+ // =====================================================================
6715
+ {
6716
+ method: "GET",
6717
+ path: `${basePath}/peer/handshake`,
6718
+ handler: async (ctx) => {
6719
+ const auth = await authenticatePeerRequest(ctx.headers);
6720
+ if (isPeerAuthRefusal(auth)) {
6721
+ return auth.body;
6722
+ }
6723
+ return {
6724
+ ok: true,
6725
+ protocol: PEER_PROTOCOL_VERSION,
6726
+ capabilities: PEER_CAPABILITIES,
6727
+ grant: {
6728
+ peerLabel: auth.grant.peerLabel,
6729
+ level: auth.grant.level,
6730
+ state: auth.grant.state,
6731
+ ledger: auth.grant.entitlement.ledger,
6732
+ },
6733
+ };
6734
+ },
6735
+ description: "Peer handshake: protocol version, capabilities, grant state",
6736
+ tags: ["claude-proxy", "sharing"],
6737
+ },
6738
+ // =====================================================================
6739
+ // POST /peer/provision -- borrower lodges a PKCE challenge
6740
+ //
6741
+ // Split provisioning: the borrower keeps the verifier and sends only its
6742
+ // digest, so the lender is never in possession of anything that could
6743
+ // become a credential. The lender authorizes in its own browser and
6744
+ // relays back a code that is useless without the verifier.
6745
+ // =====================================================================
6746
+ {
6747
+ method: "POST",
6748
+ path: `${basePath}/peer/provision`,
6749
+ handler: async (ctx) => {
6750
+ const auth = await authenticatePeerRequest(ctx.headers);
6751
+ if (isPeerAuthRefusal(auth)) {
6752
+ return auth.body;
6753
+ }
6754
+ if (auth.grant.state !== "active") {
6755
+ return buildShareRefusal(auth.grant.state === "paused" ? "paused" : "revoked", { status: 403, grant: auth.grant }).body;
6756
+ }
6757
+ if (auth.grant.level !== "complete") {
6758
+ return {
6759
+ type: "error",
6760
+ error: {
6761
+ type: "invalid_request_error",
6762
+ message: "This is a live share. Ask the lender to run " +
6763
+ "`neurolink proxy share level --to complete` first.",
6764
+ },
6765
+ };
6766
+ }
6767
+ const body = ctx.body;
6768
+ const opened = await openProvisionRequest({
6769
+ grantId: auth.grant.id,
6770
+ codeChallenge: String(body?.codeChallenge ?? ""),
6771
+ state: String(body?.state ?? ""),
6772
+ });
6773
+ if (opened.ok !== true) {
6774
+ return {
6775
+ type: "error",
6776
+ error: {
6777
+ type: "invalid_request_error",
6778
+ message: opened.reason,
6779
+ },
6780
+ };
6781
+ }
6782
+ logger.always(`[proxy] ${auth.grant.peerLabel} asked to be provisioned — run ` +
6783
+ `\`neurolink proxy share provision --peer ${auth.grant.peerLabel}\``);
6784
+ return {
6785
+ ok: true,
6786
+ status: "pending",
6787
+ expiresAt: opened.request.expiresAt,
6788
+ };
6789
+ },
6790
+ description: "Lodge a PKCE challenge for a resident credential (complete shares)",
6791
+ tags: ["claude-proxy", "sharing"],
6792
+ },
6793
+ // =====================================================================
6794
+ // GET /peer/provision -- borrower collects its authorization code
6795
+ //
6796
+ // Answers once. A code that could be claimed twice would let a replay
6797
+ // mint a second credential on the lender's account, so consumption is
6798
+ // recorded before the value is handed over.
6799
+ // =====================================================================
6800
+ {
6801
+ method: "GET",
6802
+ path: `${basePath}/peer/provision`,
6803
+ handler: async (ctx) => {
6804
+ const auth = await authenticatePeerRequest(ctx.headers);
6805
+ if (isPeerAuthRefusal(auth)) {
6806
+ return auth.body;
6807
+ }
6808
+ const claimed = await claimProvisionRequest(auth.grant.id);
6809
+ if (claimed.status !== "ready") {
6810
+ return { ok: true, status: claimed.status };
6811
+ }
6812
+ // The heartbeat address the borrower will call home on. Without it a
6813
+ // resident grant can never renew and stops at its offline grace.
6814
+ const lenderUrl = await getNodePublicUrl();
6815
+ return {
6816
+ ok: true,
6817
+ status: "ready",
6818
+ claim: buildProvisionClaim({
6819
+ grant: auth.grant,
6820
+ // The borrower renames this locally at `peer request --name`;
6821
+ // here it only has to make the token-store label unique.
6822
+ lenderName: "lender",
6823
+ ...(lenderUrl ? { lenderUrl } : {}),
6824
+ code: claimed.code,
6825
+ state: claimed.state,
6826
+ }),
6827
+ };
6828
+ },
6829
+ description: "Collect the authorization code the lender produced",
6830
+ tags: ["claude-proxy", "sharing"],
6831
+ },
6832
+ // =====================================================================
6833
+ // GET /peer/receipts -- collect signed statements of what was charged
6834
+ //
6835
+ // So the lender's word is not the only record. Each receipt carries the
6836
+ // usage it was computed from, and sequences are contiguous, so a borrower
6837
+ // can recompute every charge and see a withheld one as a gap.
6838
+ // =====================================================================
6839
+ {
6840
+ method: "GET",
6841
+ path: `${basePath}/peer/receipts`,
6842
+ handler: async (ctx) => {
6843
+ const auth = await authenticatePeerRequest(ctx.headers);
6844
+ if (isPeerAuthRefusal(auth)) {
6845
+ return auth.body;
6846
+ }
6847
+ const since = Number(ctx.query?.since ?? 0);
6848
+ const collected = await listShareReceipts(auth.grant.id, Number.isFinite(since) ? since : 0);
6849
+ return { ok: true, receipts: collected };
6850
+ },
6851
+ description: "Collect signed receipts for this grant's settled charges",
6852
+ tags: ["claude-proxy", "sharing"],
6853
+ },
6854
+ // =====================================================================
6855
+ // POST /peer/net -- settle one round of reciprocal netting
6856
+ //
6857
+ // Both sides state cumulative positions, and the round forgives the
6858
+ // overlap not yet forgiven. Cumulative rather than incremental is what
6859
+ // makes a replayed round free rather than a second payout.
6860
+ // =====================================================================
6861
+ {
6862
+ method: "POST",
6863
+ path: `${basePath}/peer/net`,
6864
+ handler: async (ctx) => {
6865
+ const auth = await authenticatePeerRequest(ctx.headers);
6866
+ if (isPeerAuthRefusal(auth)) {
6867
+ return auth.body;
6868
+ }
6869
+ const secret = auth.grant.receiptSecret;
6870
+ if (!secret) {
6871
+ return {
6872
+ type: "error",
6873
+ error: {
6874
+ type: "invalid_request_error",
6875
+ message: "This grant predates receipts and cannot be netted. " +
6876
+ "Ask the lender to re-issue it.",
6877
+ },
6878
+ };
6879
+ }
6880
+ const body = ctx.body;
6881
+ const consumedByYou = Number(body?.consumedByYou ?? NaN);
6882
+ const alreadyNetted = Number(body?.alreadyNetted ?? NaN);
6883
+ if (!Number.isFinite(consumedByYou) ||
6884
+ !Number.isFinite(alreadyNetted)) {
6885
+ return {
6886
+ type: "error",
6887
+ error: {
6888
+ type: "invalid_request_error",
6889
+ message: "A netting claim needs both cumulative totals.",
6890
+ },
6891
+ };
6892
+ }
6893
+ // The claim is the peer's own accounting, so it is signed: a figure
6894
+ // that credits the caller must not be forgeable by anyone who merely
6895
+ // reaches the port.
6896
+ const authentic = verifySharePayload({ consumedByYou, alreadyNetted, grantId: auth.grant.id }, String(body?.signature ?? ""), secret);
6897
+ if (!authentic) {
6898
+ return buildShareRefusal("unknown_token", { status: 401 }).body;
6899
+ }
6900
+ const result = await applyReciprocalNetting({
6901
+ grantId: auth.grant.id,
6902
+ consumedFromPeer: consumedByYou,
6903
+ peerAlreadyNetted: alreadyNetted,
6904
+ });
6905
+ return { ok: true, ...result };
6906
+ },
6907
+ description: "Settle one round of reciprocal netting against this grant",
6908
+ tags: ["claude-proxy", "sharing"],
6909
+ },
6910
+ // =====================================================================
6911
+ // POST /peer/note -- check or redeem a transferable coin note
6912
+ //
6913
+ // Holding the note is the credential for a check; redeeming additionally
6914
+ // needs a grant to credit. Marking spent and crediting happen under one
6915
+ // lock, so two holders racing the same note produce exactly one credit.
6916
+ // =====================================================================
6917
+ {
6918
+ method: "POST",
6919
+ path: `${basePath}/peer/note`,
6920
+ handler: async (ctx) => {
6921
+ const body = ctx.body;
6922
+ const note = decodeShareNote(String(body?.note ?? ""));
6923
+ if (!note) {
6924
+ return {
6925
+ type: "error",
6926
+ error: {
6927
+ type: "invalid_request_error",
6928
+ message: "That is not a NeuroLink coin note.",
6929
+ },
6930
+ };
6931
+ }
6932
+ const secret = await getNoteSecret();
6933
+ if (!body?.redeem) {
6934
+ // A status check needs no grant: the note itself is the credential,
6935
+ // and the answer tells a stranger nothing they did not already hold.
6936
+ const inspected = await inspectShareNote(note, secret);
6937
+ return { ok: true, ...inspected };
6938
+ }
6939
+ const auth = await authenticatePeerRequest(ctx.headers);
6940
+ if (isPeerAuthRefusal(auth)) {
6941
+ return auth.body;
6942
+ }
6943
+ if (auth.grant.entitlement.ledger !== "coins") {
6944
+ return {
6945
+ type: "error",
6946
+ error: {
6947
+ type: "invalid_request_error",
6948
+ message: "This grant is unlimited — there is no balance to redeem into.",
6949
+ },
6950
+ };
6951
+ }
6952
+ const redeemed = await redeemShareNote({
6953
+ note,
6954
+ grantId: auth.grant.id,
6955
+ secret,
6956
+ });
6957
+ if (redeemed.ok !== true) {
6958
+ return {
6959
+ ok: false,
6960
+ status: redeemed.status,
6961
+ error: {
6962
+ type: "invalid_request_error",
6963
+ message: `That note is ${redeemed.status}.`,
6964
+ },
6965
+ };
6966
+ }
6967
+ return {
6968
+ ok: true,
6969
+ status: "redeemed",
6970
+ coins: redeemed.coins,
6971
+ balance: redeemed.balance ?? null,
6972
+ };
6973
+ },
6974
+ description: "Check or redeem a transferable coin note",
6975
+ tags: ["claude-proxy", "sharing"],
6976
+ },
6977
+ // =====================================================================
6978
+ // GET /peer/limits -- what this grant may still do
6979
+ //
6980
+ // So a borrower can decide *before* spending a request whether this peer
6981
+ // is worth the extra hop. Everything here is scoped to the caller's own
6982
+ // grant: no account labels, no per-account figures, nothing that would
6983
+ // describe the lender's pool.
6984
+ // =====================================================================
6985
+ {
6986
+ method: "GET",
6987
+ path: `${basePath}/peer/limits`,
6988
+ handler: async (ctx) => {
6989
+ const auth = await authenticatePeerRequest(ctx.headers);
6990
+ if (isPeerAuthRefusal(auth)) {
6991
+ return auth.body;
6992
+ }
6993
+ const limitsRouting = runtimeConfigProvider?.();
6994
+ const snapshot = await buildPeerLimitsSnapshot(auth.grant, limitsRouting?.accountAllowlist ?? accountAllowlist);
6995
+ return { ok: true, ...snapshot };
6996
+ },
6997
+ description: "Peer limits: remaining coins, slice left, whether the grant can be served",
6998
+ tags: ["claude-proxy", "sharing"],
6999
+ },
7000
+ // =====================================================================
5951
7001
  // GET /v1/models -- List available models (Anthropic schema)
5952
7002
  //
5953
7003
  // Returns the Anthropic-shaped list response (`type`, `display_name`,
@@ -6011,6 +7061,19 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
6011
7061
  method: "GET",
6012
7062
  path: `${basePath}/limits`,
6013
7063
  handler: async (ctx) => {
7064
+ // `/limits` names every account and its quota — for an OAuth account
7065
+ // the label is the operator's email. That is an operator diagnostic,
7066
+ // not something a borrower may read, and a refresh also drives real
7067
+ // usage-API calls on the lender's accounts. Borrowed traffic gets
7068
+ // `/peer/limits`, which answers the same routing question about the
7069
+ // borrower's own grant without describing the pool.
7070
+ if (getShareContext()) {
7071
+ return buildShareRefusal("no_capacity", {
7072
+ status: 403,
7073
+ message: "This proxy does not expose pool limits to peers. " +
7074
+ "Use GET /peer/limits for what your grant may still do.",
7075
+ }).body;
7076
+ }
6014
7077
  const limitsRouting = runtimeConfigProvider?.();
6015
7078
  const effectiveAllowlist = limitsRouting?.accountAllowlist ?? accountAllowlist;
6016
7079
  // A refresh reconciles cooldowns from the fetched quota, so it makes