@askalf/dario 3.29.0 → 3.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.
package/dist/proxy.js CHANGED
@@ -11,7 +11,6 @@ import { describeTemplate, detectDrift, checkCCCompat } from './live-fingerprint
11
11
  import { AccountPool, computeStickyKey, parseRateLimits } from './pool.js';
12
12
  import { Analytics, billingBucketFromClaim } from './analytics.js';
13
13
  import { loadAllAccounts, loadAccount, refreshAccountToken } from './accounts.js';
14
- import { GroupLender, importGroupPublicKey, decodeBorrowEnvelope, parseBorrowToken, } from './sealed-pool.js';
15
14
  import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
16
15
  const ANTHROPIC_API = 'https://api.anthropic.com';
17
16
  const DEFAULT_PORT = 3456;
@@ -334,31 +333,18 @@ export function sanitizeError(err) {
334
333
  .replace(/Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]');
335
334
  }
336
335
  /**
337
- * Two-lane auth: DARIO_API_KEY (x-api-key / Authorization: Bearer) for
338
- * normal clients, and MUX_COORD_SECRET (X-Mux-Coord-Secret) for the mux
339
- * gateway forwarding a verified sealed borrow. If neither is configured
340
- * the request is allowed (loopback-only default). Exported for tests.
336
+ * API-key auth via DARIO_API_KEY (x-api-key or Authorization: Bearer).
337
+ * If unset, requests are allowed (loopback-only default). Exported for tests.
341
338
  */
342
- export function authenticateRequest(headers, apiKeyBuf, mcsBuf) {
343
- if (!apiKeyBuf && !mcsBuf)
339
+ export function authenticateRequest(headers, apiKeyBuf) {
340
+ if (!apiKeyBuf)
344
341
  return true;
345
- if (mcsBuf) {
346
- const raw = headers['x-mux-coord-secret'];
347
- const provided = typeof raw === 'string' ? raw : Array.isArray(raw) ? raw[0] : undefined;
348
- if (provided) {
349
- const providedBuf = Buffer.from(provided);
350
- if (providedBuf.length === mcsBuf.length && timingSafeEqual(providedBuf, mcsBuf))
351
- return true;
352
- }
353
- }
354
- if (apiKeyBuf) {
355
- const provided = headers['x-api-key']
356
- || headers.authorization?.replace(/^Bearer\s+/i, '');
357
- if (provided) {
358
- const providedBuf = Buffer.from(provided);
359
- if (providedBuf.length === apiKeyBuf.length && timingSafeEqual(providedBuf, apiKeyBuf))
360
- return true;
361
- }
342
+ const provided = headers['x-api-key']
343
+ || headers.authorization?.replace(/^Bearer\s+/i, '');
344
+ if (provided) {
345
+ const providedBuf = Buffer.from(provided);
346
+ if (providedBuf.length === apiKeyBuf.length && timingSafeEqual(providedBuf, apiKeyBuf))
347
+ return true;
362
348
  }
363
349
  return false;
364
350
  }
@@ -440,30 +426,6 @@ export async function startProxy(opts = {}) {
440
426
  const accountsList = await loadAllAccounts();
441
427
  const pool = accountsList.length >= 2 ? new AccountPool() : null;
442
428
  const analytics = pool ? new Analytics() : null;
443
- // Sealed-sender overflow pool — activated when ~/.dario/group.json exists.
444
- // Config format: { "groupId": "<name>", "publicKey": { n, e, modulusBytes } }
445
- // where publicKey is the GroupAdmin's exported RSA public key. Lender runs
446
- // in addition to normal pool mode — borrow requests go through a separate
447
- // /v1/pool/borrow endpoint and are verified via the admin-signed token.
448
- let groupLender = null;
449
- try {
450
- const groupConfigPath = join(homedir(), '.dario', 'group.json');
451
- const rawGroup = readFileSync(groupConfigPath, 'utf-8');
452
- const parsed = JSON.parse(rawGroup);
453
- if (parsed?.groupId && parsed.publicKey?.n && parsed.publicKey?.e && parsed.publicKey?.modulusBytes) {
454
- const pub = importGroupPublicKey(parsed.publicKey);
455
- groupLender = new GroupLender(parsed.groupId, pub);
456
- console.log(` Sealed-sender pool: group "${parsed.groupId}" loaded (${pub.modulusBytes * 8}-bit key)`);
457
- }
458
- }
459
- catch (err) {
460
- // Group config is optional — silent fallthrough if missing. Log parse
461
- // errors explicitly so a broken config doesn't fail silently.
462
- const e = err;
463
- if (e.code && e.code !== 'ENOENT') {
464
- console.warn(`[dario] group.json present but unusable: ${e.message}`);
465
- }
466
- }
467
429
  let status;
468
430
  if (pool) {
469
431
  for (const acc of accountsList) {
@@ -647,14 +609,6 @@ export async function startProxy(opts = {}) {
647
609
  // Optional proxy authentication — pre-encode key buffer for performance
648
610
  const apiKey = process.env.DARIO_API_KEY;
649
611
  const apiKeyBuf = apiKey ? Buffer.from(apiKey) : null;
650
- // Mux coord-secret — the shared secret mux uses when forwarding sealed
651
- // borrow requests to this dario acting as a lender endpoint. A request
652
- // carrying a matching X-Mux-Coord-Secret header is authenticated without
653
- // needing DARIO_API_KEY. The sealed-sender envelope has already been
654
- // verified upstream, so the coord secret is the one-hop auth between
655
- // the mux gateway and this instance.
656
- const mcs = process.env.MUX_COORD_SECRET;
657
- const mcsBuf = mcs ? Buffer.from(mcs) : null;
658
612
  // CORS origin defaults to the localhost URL the proxy is served at. Users
659
613
  // binding to a non-loopback address (e.g. a Tailscale interface) can
660
614
  // override via DARIO_CORS_ORIGIN — otherwise browser-based clients hitting
@@ -670,7 +624,7 @@ export async function startProxy(opts = {}) {
670
624
  const CORS_HEADERS = {
671
625
  'Access-Control-Allow-Origin': corsOrigin,
672
626
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
673
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta, x-mux-coord-secret',
627
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta',
674
628
  'Access-Control-Max-Age': '86400',
675
629
  ...SECURITY_HEADERS,
676
630
  };
@@ -680,7 +634,7 @@ export async function startProxy(opts = {}) {
680
634
  const ERR_FORBIDDEN = JSON.stringify({ error: 'Forbidden', message: 'Path not allowed. Supported paths: POST /v1/messages, POST /v1/chat/completions, GET /v1/models' });
681
635
  const ERR_METHOD = JSON.stringify({ error: 'Method not allowed' });
682
636
  function checkAuth(req) {
683
- return authenticateRequest(req.headers, apiKeyBuf, mcsBuf);
637
+ return authenticateRequest(req.headers, apiKeyBuf);
684
638
  }
685
639
  const server = createServer(async (req, res) => {
686
640
  if (req.method === 'OPTIONS') {
@@ -702,121 +656,6 @@ export async function startProxy(opts = {}) {
702
656
  }));
703
657
  return;
704
658
  }
705
- // Sealed-sender borrow endpoint — runs BEFORE the API-key auth check
706
- // because the admin-signed group token IS the authentication. Anyone
707
- // who presents a valid unused token can borrow capacity from this
708
- // instance's pool without also holding the local dario API key.
709
- // See src/sealed-pool.ts for the protocol.
710
- if (urlPath === '/v1/pool/borrow' && req.method === 'POST') {
711
- if (!groupLender) {
712
- res.writeHead(503, JSON_HEADERS);
713
- res.end(JSON.stringify({ error: 'sealed-sender pool not configured on this instance' }));
714
- return;
715
- }
716
- if (!pool) {
717
- res.writeHead(503, JSON_HEADERS);
718
- res.end(JSON.stringify({ error: 'pool mode required for sealed-sender borrows' }));
719
- return;
720
- }
721
- // Read body with the same limits as normal /v1/messages.
722
- const bChunks = [];
723
- let bBytes = 0;
724
- const bTimeout = setTimeout(() => { req.destroy(); }, BODY_READ_TIMEOUT_MS);
725
- try {
726
- for await (const chunk of req) {
727
- const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
728
- bBytes += buf.length;
729
- if (bBytes > MAX_BODY_BYTES) {
730
- clearTimeout(bTimeout);
731
- res.writeHead(413, JSON_HEADERS);
732
- res.end(JSON.stringify({ error: 'Request body too large' }));
733
- return;
734
- }
735
- bChunks.push(buf);
736
- }
737
- }
738
- finally {
739
- clearTimeout(bTimeout);
740
- }
741
- const envelope = decodeBorrowEnvelope(Buffer.concat(bChunks).toString('utf-8'));
742
- if (!envelope) {
743
- res.writeHead(400, JSON_HEADERS);
744
- res.end(JSON.stringify({ error: 'malformed borrow envelope' }));
745
- return;
746
- }
747
- // Envelope shape guard — envelope.request is `unknown` on the wire.
748
- // We stringify it and forward to Anthropic under the lender's identity,
749
- // so a borrower could otherwise waste the lender's rate-limit slot with
750
- // a body Anthropic will reject. Minimum: must be a plain object with
751
- // `model` (string) and `messages` (array). Anthropic validates the rest.
752
- const br = envelope.request;
753
- if (!br || typeof br !== 'object' || Array.isArray(br) ||
754
- typeof br.model !== 'string' ||
755
- !Array.isArray(br.messages)) {
756
- res.writeHead(400, JSON_HEADERS);
757
- res.end(JSON.stringify({ error: 'envelope.request must be an Anthropic /v1/messages body' }));
758
- return;
759
- }
760
- if (envelope.groupId !== groupLender.groupId) {
761
- res.writeHead(403, JSON_HEADERS);
762
- res.end(JSON.stringify({ error: 'unknown_group', expected: groupLender.groupId }));
763
- return;
764
- }
765
- const borrowTok = parseBorrowToken(envelope);
766
- if (!borrowTok) {
767
- res.writeHead(400, JSON_HEADERS);
768
- res.end(JSON.stringify({ error: 'malformed token' }));
769
- return;
770
- }
771
- const accept = groupLender.acceptBorrow(borrowTok.token, borrowTok.signature);
772
- if (!accept.ok) {
773
- res.writeHead(403, JSON_HEADERS);
774
- res.end(JSON.stringify({ error: 'borrow_rejected', reason: accept.reason }));
775
- return;
776
- }
777
- // Token validated. Forward the embedded /v1/messages request to
778
- // Anthropic using the lender's normal pool. This path is minimal:
779
- // no streaming parser, no reverse tool mapping, no 429 failover.
780
- // It's enough to demonstrate sealed-sender end-to-end; the full
781
- // feature-parity wire-up with the main /v1/messages path is a
782
- // separate change (requires threading a pre-read body through
783
- // the existing handler).
784
- const lenderAccount = pool.select();
785
- if (!lenderAccount) {
786
- res.writeHead(503, JSON_HEADERS);
787
- res.end(JSON.stringify({ error: 'lender pool exhausted' }));
788
- return;
789
- }
790
- try {
791
- const upstream = await fetch(`${ANTHROPIC_API}/v1/messages?beta=true`, {
792
- method: 'POST',
793
- headers: {
794
- 'Content-Type': 'application/json',
795
- 'authorization': `Bearer ${lenderAccount.accessToken}`,
796
- 'anthropic-version': '2023-06-01',
797
- 'anthropic-beta': 'claude-code-20250219',
798
- },
799
- body: JSON.stringify(envelope.request),
800
- });
801
- const snapshot = parseRateLimits(upstream.headers);
802
- pool.updateRateLimits(lenderAccount.alias, snapshot);
803
- const body = Buffer.from(await upstream.arrayBuffer());
804
- res.writeHead(upstream.status, {
805
- 'content-type': upstream.headers.get('content-type') ?? 'application/json',
806
- 'Access-Control-Allow-Origin': corsOrigin,
807
- ...SECURITY_HEADERS,
808
- });
809
- res.end(body);
810
- if (verbose) {
811
- console.log(`[dario] borrow: group=${envelope.groupId} → ${lenderAccount.alias} (${upstream.status}, ${body.length}B)`);
812
- }
813
- }
814
- catch (err) {
815
- res.writeHead(502, JSON_HEADERS);
816
- res.end(JSON.stringify({ error: 'upstream_error', message: err.message }));
817
- }
818
- return;
819
- }
820
659
  if (!checkAuth(req)) {
821
660
  res.writeHead(401, JSON_HEADERS);
822
661
  res.end(ERR_UNAUTH);
@@ -852,10 +691,6 @@ export async function startProxy(opts = {}) {
852
691
  mode: 'pool',
853
692
  ...pool.status(),
854
693
  stickyBindings: pool.stickyCount(),
855
- sealedSender: groupLender ? {
856
- groupId: groupLender.groupId,
857
- seenTokens: groupLender.seenCount(),
858
- } : null,
859
694
  accounts,
860
695
  }));
861
696
  return;
@@ -1807,19 +1642,13 @@ export async function startProxy(opts = {}) {
1807
1642
  if (!isLoopbackHost(host)) {
1808
1643
  console.log('');
1809
1644
  console.log(` ⚠ Bound to ${host} — reachable from other machines on the network.`);
1810
- if (!apiKey && !mcs) {
1645
+ if (!apiKey) {
1811
1646
  console.log(' No auth configured. Any host that can reach this port can proxy');
1812
- console.log(' requests through your OAuth subscription. Set DARIO_API_KEY (for');
1813
- console.log(' normal clients) or MUX_COORD_SECRET (for mux lender mode) before');
1814
- console.log(' exposing dario beyond loopback.');
1647
+ console.log(' requests through your OAuth subscription. Set DARIO_API_KEY');
1648
+ console.log(' before exposing dario beyond loopback.');
1815
1649
  }
1816
1650
  else {
1817
- const lanes = [];
1818
- if (apiKey)
1819
- lanes.push('x-api-key / Authorization (DARIO_API_KEY)');
1820
- if (mcs)
1821
- lanes.push('X-Mux-Coord-Secret (MUX_COORD_SECRET — mux lender mode)');
1822
- console.log(` Auth required — accepted credentials: ${lanes.join(' or ')}.`);
1651
+ console.log(' Auth required accepted credentials: x-api-key / Authorization (DARIO_API_KEY).');
1823
1652
  }
1824
1653
  }
1825
1654
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.29.0",
3
+ "version": "3.30.0",
4
4
  "description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsc && cp src/cc-template-data.json dist/ && node -e \"require('fs').mkdirSync('dist/shim',{recursive:true})\" && cp src/shim/runtime.cjs dist/shim/",
24
- "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/mux-coord-secret.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/pacing.mjs && node test/stream-drain.mjs && node test/subagent.mjs && node test/mcp-protocol.mjs && node test/mcp-tools.mjs && node test/mcp-e2e.mjs && node test/session-rotation.mjs && node test/drift-detection.mjs && node test/cc-authorize-probe-classifier.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs",
24
+ "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/pacing.mjs && node test/stream-drain.mjs && node test/subagent.mjs && node test/mcp-protocol.mjs && node test/mcp-tools.mjs && node test/mcp-e2e.mjs && node test/session-rotation.mjs && node test/drift-detection.mjs && node test/cc-authorize-probe-classifier.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs",
25
25
  "audit": "npm audit --production --audit-level=high",
26
26
  "prepublishOnly": "npm run build",
27
27
  "start": "node dist/cli.js",
@@ -1,202 +0,0 @@
1
- /**
2
- * Sealed-sender overflow pool — RSA blind signatures for unlinkable capacity
3
- * sharing inside a trust group.
4
- *
5
- * The problem this solves: in a federated pool where several friends lend
6
- * each other account capacity, a naive design leaks who is borrowing what.
7
- * If member A sends "I'm borrowing from your pool" to member B's dario
8
- * instance, B learns exactly which of their friends is running which
9
- * workload — and over time B can build a pretty detailed surveillance log
10
- * of everyone else's agent sessions. That's the opposite of what a private
11
- * friend pool should provide.
12
- *
13
- * The solution is Chaum's 1983 blind signature construction. A trusted
14
- * admin (one member of the group, selected by social consensus) issues
15
- * signed borrow tokens to each member. The admin never sees the token
16
- * values — they sign blinded values, and the blinding is unlinkable at
17
- * the cryptographic level. When a member sends a token to a lender, the
18
- * lender can verify "this was signed by the group admin" without learning
19
- * WHICH member holds that token. The lender sees a valid group credential
20
- * and nothing more.
21
- *
22
- * From Anthropic's perspective nothing changes: the request still hits
23
- * their API under the lender's identity, fully attributable to a real
24
- * paying Max subscriber. The privacy property is entirely INSIDE the
25
- * trust group — no member can surveil another member's usage through
26
- * the pool layer.
27
- *
28
- * What this is NOT: this is not anonymity from Anthropic, not onion
29
- * routing, not credential laundering. It is a privacy layer on top of
30
- * a legitimate friends-pool arrangement. Members opt in, the admin is
31
- * known, membership is revocable by rotating the group key. It's the
32
- * same trust model as a family Netflix account, with unlinkability as
33
- * a feature for the pool's internal telemetry.
34
- *
35
- * Implementation notes:
36
- * - RSA-2048 with FDH (full-domain hash) padding via MGF1-SHA256.
37
- * - Node's crypto.publicEncrypt / privateDecrypt with RSA_NO_PADDING
38
- * for raw RSA operations. All modular arithmetic happens in BigInt.
39
- * - Tokens are 32 random bytes each, single-use (lender tracks SHA-256
40
- * hashes of seen tokens to prevent double-spend).
41
- * - Admin does not need to be online for members to use tokens. Admin
42
- * only runs when issuing a new batch (typically once per day/week).
43
- */
44
- import { type KeyObject } from 'node:crypto';
45
- export interface RSAPublicKey {
46
- n: bigint;
47
- e: bigint;
48
- modulusBytes: number;
49
- keyObj: KeyObject;
50
- }
51
- export interface RSAPrivateKey extends RSAPublicKey {
52
- keyObjPriv: KeyObject;
53
- }
54
- /**
55
- * Blind a token: pick r ∈ [2, n), compute blinded = FDH(token) · r^e mod n.
56
- * The admin sees only the blinded value (uniform over Z_n*) and learns
57
- * nothing about the token.
58
- */
59
- export declare function blindToken(tokenBytes: Buffer, pubKey: RSAPublicKey): {
60
- blinded: bigint;
61
- r: bigint;
62
- };
63
- /** Admin-side: sign a blinded value. No knowledge of the original token. */
64
- export declare function signBlinded(blinded: bigint, privKey: RSAPrivateKey): bigint;
65
- /**
66
- * Member-side: given the admin's signature on the blinded value, remove
67
- * the blinding factor to obtain a raw RSA-FDH signature over the original
68
- * token that the admin never saw.
69
- *
70
- * Math: signed_blinded = (FDH(t) · r^e)^d = FDH(t)^d · r mod n.
71
- * Multiplying by r^(-1) mod n yields FDH(t)^d = the raw signature.
72
- */
73
- export declare function unblindSignature(blindedSignature: bigint, r: bigint, pubKey: RSAPublicKey): bigint;
74
- /**
75
- * Verify a (token, signature) pair against the admin's public key.
76
- * True iff signature^e ≡ FDH(token) (mod n).
77
- */
78
- export declare function verifyTokenSignature(tokenBytes: Buffer, signature: bigint, pubKey: RSAPublicKey): boolean;
79
- export declare function generateGroupKey(bits?: number): RSAPrivateKey;
80
- export interface ExportedGroupKey {
81
- n: string;
82
- e: string;
83
- modulusBytes: number;
84
- }
85
- export declare function exportGroupPublicKey(key: RSAPublicKey): ExportedGroupKey;
86
- export declare function importGroupPublicKey(exported: ExportedGroupKey): RSAPublicKey;
87
- export interface MemberRecord {
88
- pubkey: string;
89
- expiresAt: number;
90
- quotaPerBatch: number;
91
- }
92
- /**
93
- * Admin holds the group private key and a roster of authorized members.
94
- * Admin does NOT hold any of the tokens, by design — blind signing means
95
- * the admin never sees what they signed. This is the key privacy property.
96
- *
97
- * The admin's responsibilities are purely social: decide who's in the
98
- * group, set per-member quotas, rotate the group key when someone leaves.
99
- */
100
- export declare class GroupAdmin {
101
- readonly groupId: string;
102
- readonly key: RSAPrivateKey;
103
- readonly members: Map<string, MemberRecord>;
104
- constructor(groupId: string, key: RSAPrivateKey, members: Map<string, MemberRecord>);
105
- static create(groupId: string, bits?: number): GroupAdmin;
106
- addMember(pubkey: string, quotaPerBatch?: number, validForDays?: number): void;
107
- removeMember(pubkey: string): boolean;
108
- /**
109
- * Sign a batch of blinded tokens submitted by a member. The admin
110
- * authenticates the request out-of-band (member identity auth happens
111
- * at the HTTP layer via a member signing key — not modelled here).
112
- *
113
- * Throws on: unknown member, expired membership, batch-too-large.
114
- */
115
- signBatch(memberPubkey: string, blinded: bigint[]): bigint[];
116
- publicKey(): ExportedGroupKey;
117
- }
118
- export interface PreparedBatch {
119
- blinded: bigint[];
120
- state: Array<{
121
- token: Buffer;
122
- r: bigint;
123
- }>;
124
- }
125
- export interface BorrowToken {
126
- token: Buffer;
127
- signature: bigint;
128
- }
129
- /**
130
- * Member holds an identity pubkey (used by the admin for roster lookup)
131
- * and a local stash of unused (token, signature) pairs. Tokens are single-
132
- * use — consume one per borrow. Admin never saw any of these tokens.
133
- */
134
- export declare class GroupMember {
135
- readonly memberPubkey: string;
136
- readonly groupPublicKey: RSAPublicKey;
137
- private tokens;
138
- constructor(memberPubkey: string, groupPublicKey: RSAPublicKey);
139
- /**
140
- * Step 1 of a token batch: generate random tokens, blind each, return
141
- * the blinded values (to send to admin) plus the per-token state
142
- * (kept locally for unblinding after admin responds).
143
- */
144
- prepareBatch(count: number): PreparedBatch;
145
- /**
146
- * Step 2 of a token batch: unblind each admin-signed value, verify the
147
- * resulting raw signature, and add the (token, signature) pair to the
148
- * local stash. Any verification failure throws — we never store a token
149
- * whose signature doesn't check out.
150
- */
151
- finalizeBatch(signedBlinded: bigint[], state: Array<{
152
- token: Buffer;
153
- r: bigint;
154
- }>): void;
155
- consumeToken(): BorrowToken | null;
156
- tokenCount(): number;
157
- }
158
- export type AcceptResult = {
159
- ok: true;
160
- } | {
161
- ok: false;
162
- reason: 'invalid_signature' | 'double_spend' | 'malformed';
163
- };
164
- /**
165
- * Lender holds the group public key and a set of token hashes that have
166
- * already been redeemed. Memory-only in v1; a persisted set would be a
167
- * sqlite table keyed on token hash, or just a file of sha256 hex lines.
168
- *
169
- * The lender LEARNS NOTHING about which member borrowed. That's the
170
- * whole point — blind signatures decouple "who signed this request"
171
- * (the admin, uniformly) from "who holds the token" (one specific
172
- * member who is anonymous to the lender).
173
- */
174
- export declare class GroupLender {
175
- readonly groupId: string;
176
- readonly groupPublicKey: RSAPublicKey;
177
- private seenTokens;
178
- private maxSeenTokens;
179
- constructor(groupId: string, groupPublicKey: RSAPublicKey, opts?: {
180
- maxSeenTokens?: number;
181
- });
182
- acceptBorrow(token: Buffer, signature: bigint): AcceptResult;
183
- seenCount(): number;
184
- }
185
- /**
186
- * Envelope the member sends to a lender's /v1/pool/borrow endpoint. The
187
- * `request` field carries an embedded Anthropic /v1/messages body that
188
- * the lender will proxy to api.anthropic.com under its own identity.
189
- *
190
- * Version field lets us rotate crypto or protocol details without
191
- * breaking older members in the same group.
192
- */
193
- export interface BorrowEnvelope {
194
- v: 1;
195
- groupId: string;
196
- token: string;
197
- sig: string;
198
- request: unknown;
199
- }
200
- export declare function encodeBorrowEnvelope(groupId: string, bt: BorrowToken, request: unknown): string;
201
- export declare function decodeBorrowEnvelope(s: string): BorrowEnvelope | null;
202
- export declare function parseBorrowToken(env: BorrowEnvelope): BorrowToken | null;