W
File size: 5,815 Bytes
2b64d42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { callerKeyFromRequest, extractBodyCallerSubKey, hasCallerScope } from '../src/caller-key.js';

function fakeReq({ headers = {}, ip = '127.0.0.1' } = {}) {
  return { headers, socket: { remoteAddress: ip } };
}

describe('extractBodyCallerSubKey (v2.0.25 HIGH-3)', () => {
  it('returns empty for body with no user signal', () => {
    assert.equal(extractBodyCallerSubKey({}), '');
    assert.equal(extractBodyCallerSubKey(null), '');
    assert.equal(extractBodyCallerSubKey({ messages: [], model: 'm' }), '');
  });

  it('returns a digest when body.user is present (OpenAI chat convention)', () => {
    const k = extractBodyCallerSubKey({ user: 'alice@example.com' });
    assert.ok(k && k.length === 16);
  });

  it('different users yield different digests', () => {
    const a = extractBodyCallerSubKey({ user: 'alice' });
    const b = extractBodyCallerSubKey({ user: 'bob' });
    assert.notEqual(a, b);
  });

  it('uses Responses-style previous_response_id when no user', () => {
    const k = extractBodyCallerSubKey({ previous_response_id: 'resp_abc123' });
    assert.ok(k && k.length === 16);
  });

  it('does NOT inspect metadata.user_id (handled by messages.js parser)', () => {
    // metadata.user_id is the Anthropic Claude Code device id field; the
    // /v1/messages handler has a specialized parser for its JSON-encoded
    // shape and appends `:user:` itself. Two-handler stamping would
    // double-prefix the callerKey, so caller-key.js stays out of it.
    assert.equal(extractBodyCallerSubKey({ metadata: { user_id: '{"device_id":"abc"}' } }), '');
  });
});

describe('callerKeyFromRequest with body', () => {
  it('appends :user:<digest> when body has a user signal', () => {
    const k = callerKeyFromRequest(fakeReq(), 'sk-test-key', { user: 'alice' });
    assert.match(k, /^api:[a-f0-9]+:user:[a-f0-9]{16}$/);
  });

  it('falls back to :client:<ip+ua-digest> when body has no user signal', () => {
    // v2.0.37 (#93 follow-up): bare apiKey + no body user used to drop
    // straight to "shared API key, no per-user scope" and disable
    // cascade reuse. Now we synthesize a stable per-physical-client
    // subkey from IP + UA so single-user self-hosted setups can reuse.
    const k = callerKeyFromRequest(fakeReq(), 'sk-test-key', {});
    assert.match(k, /^api:[a-f0-9]+:client:[a-f0-9]{16}$/);
  });

  it('two end-users on same shared API key get different keys', () => {
    const ka = callerKeyFromRequest(fakeReq(), 'shared-key', { user: 'alice' });
    const kb = callerKeyFromRequest(fakeReq(), 'shared-key', { user: 'bob' });
    assert.notEqual(ka, kb);
  });

  it('two physical clients on same apiKey land on different subkeys via IP+UA', () => {
    // The v2.0.37 fallback must not collapse distinct clients into one
    // pool — that would re-introduce the cross-user cascade bleed
    // v2.0.25 originally guarded against.
    const ka = callerKeyFromRequest(
      fakeReq({ ip: '1.2.3.4', headers: { 'user-agent': 'claude-cli/1.0' } }),
      'shared-key', null,
    );
    const kb = callerKeyFromRequest(
      fakeReq({ ip: '5.6.7.8', headers: { 'user-agent': 'claude-cli/1.0' } }),
      'shared-key', null,
    );
    assert.notEqual(ka, kb);
    assert.match(ka, /^api:[a-f0-9]+:client:[a-f0-9]{16}$/);
  });

  it('same physical client across turns lands on the same subkey (reuse precondition)', () => {
    // The whole point of the v2.0.37 fallback: stable identity across
    // requests so the cascade pool actually finds the prior entry.
    const ka = callerKeyFromRequest(
      fakeReq({ ip: '1.2.3.4', headers: { 'user-agent': 'claude-cli/1.0' } }),
      'shared-key', null,
    );
    const kb = callerKeyFromRequest(
      fakeReq({ ip: '1.2.3.4', headers: { 'user-agent': 'claude-cli/1.0' } }),
      'shared-key', null,
    );
    assert.equal(ka, kb);
  });

  it('omits :client: when no IP and no UA are extractable', () => {
    // Defensive: if both IP and UA are empty strings the fallback
    // produces no useful identity so we fall back to the bare apiKey.
    const k = callerKeyFromRequest({ headers: {} }, 'sk-test-key', {});
    assert.match(k, /^api:[a-f0-9]+$/);
  });

  it('falls back to header session id when no API key', () => {
    const k = callerKeyFromRequest(fakeReq({ headers: { 'x-session-id': 'sess-xyz' } }));
    assert.match(k, /^session:[a-f0-9]+$/);
  });

  it('falls back to ip+ua when nothing else available', () => {
    const k = callerKeyFromRequest(fakeReq({ ip: '1.2.3.4', headers: { 'user-agent': 'Mozilla/X' } }));
    assert.match(k, /^client:[a-f0-9]+$/);
  });
});

describe('hasCallerScope', () => {
  it('true for callerKey containing :user:', () => {
    assert.equal(hasCallerScope('api:abc:user:xyz'), true);
  });

  it('true for callerKey containing :client: anywhere (v2.0.37 fallback)', () => {
    // apiKey-mode now appends `:client:<ip+ua-hash>` — scope check
    // must recognize the segment anywhere, not just as a prefix.
    assert.equal(hasCallerScope('api:abc:client:xyz'), true);
  });

  it('true for session: prefix', () => {
    assert.equal(hasCallerScope('session:abc'), true);
  });

  it('true for client: prefix', () => {
    assert.equal(hasCallerScope('client:abc'), true);
  });

  it('false for bare api: without any subkey', () => {
    // Should never happen in practice now (callerKeyFromRequest
    // always tries to add a :client: or :user: subkey) but if some
    // path fabricates a bare key, scope is still rejected.
    assert.equal(hasCallerScope('api:abc'), false);
  });

  it('true when body carries a user signal even if callerKey is bare', () => {
    assert.equal(hasCallerScope('api:abc', null, { user: 'alice' }), true);
  });
});