@openzeppelin/guardian-client 0.18.0 → 0.18.2
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/README.md +52 -4
- package/dist/conversion.d.ts +4 -2
- package/dist/conversion.d.ts.map +1 -1
- package/dist/conversion.js +22 -0
- package/dist/conversion.js.map +1 -1
- package/dist/error-codes.d.ts +1 -1
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +3 -0
- package/dist/error-codes.js.map +1 -1
- package/dist/http.d.ts +46 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +175 -26
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/server-types.d.ts +35 -2
- package/dist/server-types.d.ts.map +1 -1
- package/dist/types.d.ts +104 -9
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/conversion.test.ts +33 -1
- package/src/conversion.ts +30 -0
- package/src/error-codes.ts +3 -0
- package/src/http.test.ts +461 -1
- package/src/http.ts +216 -25
- package/src/index.ts +6 -0
- package/src/server-types.ts +40 -2
- package/src/types.ts +123 -9
package/src/http.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
3
|
import { GuardianHttpClient, GuardianHttpError } from './http.js';
|
|
4
|
-
import type { Signer, ConfigureResponse, StateObject, DeltaObject, DeltaProposalResponse } from './types.js';
|
|
4
|
+
import type { Signer, ConfigureResponse, StateObject, DeltaObject, DeltaProposalResponse, SessionRequestSigner } from './types.js';
|
|
5
5
|
|
|
6
6
|
// Mock fetch globally
|
|
7
7
|
const mockFetch = vi.fn();
|
|
@@ -1632,3 +1632,463 @@ describe('GuardianHttpError', () => {
|
|
|
1632
1632
|
});
|
|
1633
1633
|
});
|
|
1634
1634
|
});
|
|
1635
|
+
|
|
1636
|
+
describe('GuardianHttpClient sessions', () => {
|
|
1637
|
+
const sessionPublicKey = '0x02' + 'c'.repeat(64);
|
|
1638
|
+
const stateBody = {
|
|
1639
|
+
account_id: '0xabc',
|
|
1640
|
+
commitment: '0x' + '0'.repeat(64),
|
|
1641
|
+
state_json: { data: 'AAAA' },
|
|
1642
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
1643
|
+
updated_at: '2026-01-01T00:00:00Z',
|
|
1644
|
+
auth_scheme: 'falcon',
|
|
1645
|
+
};
|
|
1646
|
+
let client: GuardianHttpClient;
|
|
1647
|
+
let session: SessionRequestSigner;
|
|
1648
|
+
|
|
1649
|
+
function sessionExpiringIn(seconds: number): SessionRequestSigner {
|
|
1650
|
+
return {
|
|
1651
|
+
publicKey: sessionPublicKey,
|
|
1652
|
+
signerCommitment: mockSigner.commitment,
|
|
1653
|
+
expiresAt: Math.floor(Date.now() / 1000) + seconds,
|
|
1654
|
+
signRequest: vi.fn().mockReturnValue('0x' + 'd'.repeat(128)),
|
|
1655
|
+
signLogout: vi.fn().mockReturnValue('0x' + 'e'.repeat(128)),
|
|
1656
|
+
onEnded: vi.fn(),
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function lastHeaders(): Record<string, string> {
|
|
1661
|
+
return mockFetch.mock.calls.at(-1)![1].headers as Record<string, string>;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function rejectWith(code: string, status = 401): void {
|
|
1665
|
+
mockFetch.mockResolvedValueOnce({
|
|
1666
|
+
ok: false,
|
|
1667
|
+
headers: new Headers(),
|
|
1668
|
+
status,
|
|
1669
|
+
statusText: 'Unauthorized',
|
|
1670
|
+
text: async () => JSON.stringify({ code, message: code, meta: { retryable: false } }),
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
beforeEach(() => {
|
|
1675
|
+
client = new GuardianHttpClient('http://localhost:3000');
|
|
1676
|
+
client.setSigner(mockSigner);
|
|
1677
|
+
session = sessionExpiringIn(3600);
|
|
1678
|
+
mockFetch.mockReset();
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
afterEach(() => {
|
|
1682
|
+
vi.clearAllMocks();
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
it('signs every session-eligible route with the session key', async () => {
|
|
1686
|
+
client.setSession(session);
|
|
1687
|
+
const accountId = '0xabc';
|
|
1688
|
+
const calls: Array<[string, () => Promise<unknown>]> = [
|
|
1689
|
+
['getState', () => client.getState(accountId)],
|
|
1690
|
+
['getCanonicalNonce', () => client.getCanonicalNonce(accountId)],
|
|
1691
|
+
['getDelta', () => client.getDelta(accountId, 1)],
|
|
1692
|
+
['getDeltaSince', () => client.getDeltaSince(accountId, 1)],
|
|
1693
|
+
['getDeltaHistory', () => client.getDeltaHistory(accountId)],
|
|
1694
|
+
['getDeltaProposals', () => client.getDeltaProposals(accountId)],
|
|
1695
|
+
['getDeltaProposal', () => client.getDeltaProposal(accountId, '0x' + 'e'.repeat(64))],
|
|
1696
|
+
[
|
|
1697
|
+
'pushDeltaProposal',
|
|
1698
|
+
() =>
|
|
1699
|
+
client.pushDeltaProposal({
|
|
1700
|
+
accountId,
|
|
1701
|
+
nonce: 1,
|
|
1702
|
+
deltaPayload: { txSummary: { data: 'AAAA' }, signatures: [] },
|
|
1703
|
+
}),
|
|
1704
|
+
],
|
|
1705
|
+
[
|
|
1706
|
+
'signDeltaProposal',
|
|
1707
|
+
() =>
|
|
1708
|
+
client.signDeltaProposal({
|
|
1709
|
+
accountId,
|
|
1710
|
+
commitment: '0x' + 'e'.repeat(64),
|
|
1711
|
+
signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) },
|
|
1712
|
+
}),
|
|
1713
|
+
],
|
|
1714
|
+
];
|
|
1715
|
+
|
|
1716
|
+
for (const [name, call] of calls) {
|
|
1717
|
+
// Only the request headers matter; the empty body may fail to parse.
|
|
1718
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
|
|
1719
|
+
await call().catch(() => undefined);
|
|
1720
|
+
expect(lastHeaders()['x-auth-format'], name).toBe('session');
|
|
1721
|
+
expect(lastHeaders()['x-pubkey'], name).toBe(sessionPublicKey);
|
|
1722
|
+
}
|
|
1723
|
+
expect(session.signRequest).toHaveBeenCalledTimes(calls.length);
|
|
1724
|
+
expect(mockSigner.signRequest).not.toHaveBeenCalled();
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
it('does not use a session without a wallet signer', () => {
|
|
1728
|
+
const unsigned = new GuardianHttpClient('http://localhost:3000');
|
|
1729
|
+
unsigned.setSession(session);
|
|
1730
|
+
expect(unsigned.getSession()).toBeNull();
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
it('re-signs a replay-rejected request with the session', async () => {
|
|
1734
|
+
client.setSession(session);
|
|
1735
|
+
mockFetch.mockResolvedValueOnce({
|
|
1736
|
+
ok: false,
|
|
1737
|
+
headers: new Headers(),
|
|
1738
|
+
status: 401,
|
|
1739
|
+
statusText: 'Unauthorized',
|
|
1740
|
+
text: async () =>
|
|
1741
|
+
JSON.stringify({
|
|
1742
|
+
code: 'authentication_replay',
|
|
1743
|
+
message: 'replay',
|
|
1744
|
+
meta: { retryable: true },
|
|
1745
|
+
}),
|
|
1746
|
+
});
|
|
1747
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => stateBody });
|
|
1748
|
+
|
|
1749
|
+
await client.getState('0xabc');
|
|
1750
|
+
|
|
1751
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
1752
|
+
const [first, second] = mockFetch.mock.calls.map(([, init]) => init.headers);
|
|
1753
|
+
expect(second['x-auth-format']).toBe('session');
|
|
1754
|
+
expect(second['x-timestamp']).not.toBe(first['x-timestamp']);
|
|
1755
|
+
expect(session.signRequest).toHaveBeenCalledTimes(2);
|
|
1756
|
+
expect(client.getSession()).toBe(session);
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
it('does not use a session granted by another wallet', async () => {
|
|
1760
|
+
client.setSession({ ...session, signerCommitment: '0x' + '9'.repeat(64) });
|
|
1761
|
+
expect(client.getSession()).toBeNull();
|
|
1762
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => stateBody });
|
|
1763
|
+
|
|
1764
|
+
await client.getState('0xabc');
|
|
1765
|
+
|
|
1766
|
+
expect(lastHeaders()['x-pubkey']).toBe(mockSigner.publicKey);
|
|
1767
|
+
expect(session.signRequest).not.toHaveBeenCalled();
|
|
1768
|
+
expect(session.onEnded).not.toHaveBeenCalled();
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
it('signs per-account requests with the session key', async () => {
|
|
1772
|
+
client.setSession(session);
|
|
1773
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => stateBody });
|
|
1774
|
+
|
|
1775
|
+
await client.getState('0xabc');
|
|
1776
|
+
|
|
1777
|
+
expect(lastHeaders()).toMatchObject({
|
|
1778
|
+
'x-pubkey': sessionPublicKey,
|
|
1779
|
+
'x-signature': '0x' + 'd'.repeat(128),
|
|
1780
|
+
'x-auth-format': 'session',
|
|
1781
|
+
});
|
|
1782
|
+
expect(session.signRequest).toHaveBeenCalledOnce();
|
|
1783
|
+
expect(mockSigner.signRequest).not.toHaveBeenCalled();
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
it('keeps configure on the wallet while a session is set', async () => {
|
|
1787
|
+
client.setSession(session);
|
|
1788
|
+
mockFetch.mockResolvedValueOnce({
|
|
1789
|
+
ok: true,
|
|
1790
|
+
json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x01' }),
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
await client.configure({
|
|
1794
|
+
accountId: '0xabc',
|
|
1795
|
+
auth: { MidenFalconRpo: { cosigner_commitments: [] } },
|
|
1796
|
+
initialState: { data: 'AAAA', accountId: '0xabc' },
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
expect(lastHeaders()['x-pubkey']).toBe(mockSigner.publicKey);
|
|
1800
|
+
expect(lastHeaders()['x-auth-format']).toBeUndefined();
|
|
1801
|
+
expect(session.signRequest).not.toHaveBeenCalled();
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
it('keeps pushDelta and abandonCandidate on the wallet while a session is set', async () => {
|
|
1805
|
+
client.setSession(session);
|
|
1806
|
+
mockFetch.mockResolvedValueOnce({
|
|
1807
|
+
ok: true,
|
|
1808
|
+
json: async () => ({
|
|
1809
|
+
account_id: '0xabc',
|
|
1810
|
+
nonce: 1,
|
|
1811
|
+
new_commitment: '0x' + 'd'.repeat(64),
|
|
1812
|
+
ack_sig: '0x01',
|
|
1813
|
+
ack_pubkey: '0x02',
|
|
1814
|
+
ack_scheme: 'falcon',
|
|
1815
|
+
}),
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
await client.pushDelta({
|
|
1819
|
+
accountId: '0xabc',
|
|
1820
|
+
nonce: 1,
|
|
1821
|
+
prevCommitment: '0x' + 'b'.repeat(64),
|
|
1822
|
+
deltaPayload: { data: 'base64summary' },
|
|
1823
|
+
status: {
|
|
1824
|
+
status: 'pending' as const,
|
|
1825
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1826
|
+
proposerId: '0x' + 'c'.repeat(64),
|
|
1827
|
+
cosignerSigs: [],
|
|
1828
|
+
},
|
|
1829
|
+
});
|
|
1830
|
+
expect(lastHeaders()['x-pubkey']).toBe(mockSigner.publicKey);
|
|
1831
|
+
expect(lastHeaders()['x-auth-format']).toBeUndefined();
|
|
1832
|
+
|
|
1833
|
+
mockFetch.mockResolvedValueOnce({
|
|
1834
|
+
ok: true,
|
|
1835
|
+
json: async () => ({
|
|
1836
|
+
account_id: '0xabc',
|
|
1837
|
+
nonce: 1,
|
|
1838
|
+
state: 'abandon_requested',
|
|
1839
|
+
abandon_requested_at: '2024-01-01T00:00:00Z',
|
|
1840
|
+
}),
|
|
1841
|
+
});
|
|
1842
|
+
await client.abandonCandidate('0xabc', 1);
|
|
1843
|
+
expect(lastHeaders()['x-pubkey']).toBe(mockSigner.publicKey);
|
|
1844
|
+
expect(lastHeaders()['x-auth-format']).toBeUndefined();
|
|
1845
|
+
expect(session.signRequest).not.toHaveBeenCalled();
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
it('revokes every session with a wallet signature and stops using the session', async () => {
|
|
1849
|
+
const wallet: Signer = {
|
|
1850
|
+
...mockSigner,
|
|
1851
|
+
signSessionRevokeAll: vi.fn().mockReturnValue('0x' + '7'.repeat(128)),
|
|
1852
|
+
};
|
|
1853
|
+
client.setSigner(wallet);
|
|
1854
|
+
client.setSession(session);
|
|
1855
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ revoked: 2 }) });
|
|
1856
|
+
|
|
1857
|
+
await expect(client.revokeAllSessions()).resolves.toBe(2);
|
|
1858
|
+
|
|
1859
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
1860
|
+
expect(url).toBe('http://localhost:3000/session/revoke-all');
|
|
1861
|
+
expect(init.method).toBe('POST');
|
|
1862
|
+
expect(JSON.parse(init.body)).toEqual({ signer_commitment: mockSigner.commitment });
|
|
1863
|
+
expect(init.headers).toMatchObject({
|
|
1864
|
+
'x-pubkey': mockSigner.publicKey,
|
|
1865
|
+
'x-signature': '0x' + '7'.repeat(128),
|
|
1866
|
+
});
|
|
1867
|
+
expect(init.headers['x-auth-format']).toBeUndefined();
|
|
1868
|
+
const timestamp = Number(init.headers['x-timestamp']);
|
|
1869
|
+
expect(wallet.signSessionRevokeAll).toHaveBeenCalledWith(mockSigner.commitment, timestamp);
|
|
1870
|
+
expect(client.getSession()).toBeNull();
|
|
1871
|
+
expect(session.onEnded).toHaveBeenCalledExactlyOnceWith('logout');
|
|
1872
|
+
expect(session.signRequest).not.toHaveBeenCalled();
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
it('keeps a session started while revoke-all is in flight', async () => {
|
|
1876
|
+
client.setSigner({
|
|
1877
|
+
...mockSigner,
|
|
1878
|
+
signSessionRevokeAll: vi.fn().mockReturnValue('0x' + '7'.repeat(128)),
|
|
1879
|
+
});
|
|
1880
|
+
client.setSession(session);
|
|
1881
|
+
let respond!: (value: unknown) => void;
|
|
1882
|
+
mockFetch.mockReturnValueOnce(new Promise((resolve) => (respond = resolve)));
|
|
1883
|
+
const pending = client.revokeAllSessions();
|
|
1884
|
+
await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledOnce());
|
|
1885
|
+
|
|
1886
|
+
const started = sessionExpiringIn(3600);
|
|
1887
|
+
client.setSession(started);
|
|
1888
|
+
respond({ ok: true, json: async () => ({ revoked: 1 }) });
|
|
1889
|
+
await expect(pending).resolves.toBe(1);
|
|
1890
|
+
|
|
1891
|
+
expect(client.getSession()).toBe(started);
|
|
1892
|
+
expect(started.onEnded).not.toHaveBeenCalled();
|
|
1893
|
+
expect(session.onEnded).toHaveBeenCalledExactlyOnceWith('logout');
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
it('keeps the session when revoke-all fails', async () => {
|
|
1897
|
+
client.setSigner({
|
|
1898
|
+
...mockSigner,
|
|
1899
|
+
signSessionRevokeAll: vi.fn().mockReturnValue('0x' + '7'.repeat(128)),
|
|
1900
|
+
});
|
|
1901
|
+
client.setSession(session);
|
|
1902
|
+
mockFetch.mockRejectedValueOnce(new TypeError('network down'));
|
|
1903
|
+
|
|
1904
|
+
await expect(client.revokeAllSessions()).rejects.toThrow();
|
|
1905
|
+
|
|
1906
|
+
expect(client.getSession()).toBe(session);
|
|
1907
|
+
expect(session.onEnded).not.toHaveBeenCalled();
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
it('drops a session the server ended and lets the wallet sign the next request', async () => {
|
|
1911
|
+
const reasons = {
|
|
1912
|
+
session_revoked: 'revoked',
|
|
1913
|
+
session_expired: 'expired',
|
|
1914
|
+
authentication_failed: 'rejected',
|
|
1915
|
+
} as const;
|
|
1916
|
+
for (const [code, reason] of Object.entries(reasons)) {
|
|
1917
|
+
const current = sessionExpiringIn(3600);
|
|
1918
|
+
client.setSession(current);
|
|
1919
|
+
rejectWith(code);
|
|
1920
|
+
|
|
1921
|
+
await expect(client.getState('0xabc')).rejects.toMatchObject({ code });
|
|
1922
|
+
expect(client.getSession()).toBeNull();
|
|
1923
|
+
expect(current.onEnded).toHaveBeenCalledExactlyOnceWith(reason);
|
|
1924
|
+
|
|
1925
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => stateBody });
|
|
1926
|
+
await client.getState('0xabc');
|
|
1927
|
+
expect(lastHeaders()['x-pubkey']).toBe(mockSigner.publicKey);
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
it('keeps the session on errors that do not end it', async () => {
|
|
1932
|
+
client.setSession(session);
|
|
1933
|
+
// `authorization_failed`: the signer does not cosign this account, but the
|
|
1934
|
+
// session still works for its other accounts.
|
|
1935
|
+
for (const code of ['wallet_signature_required', 'authorization_failed']) {
|
|
1936
|
+
rejectWith(code, 403);
|
|
1937
|
+
|
|
1938
|
+
await expect(client.getState('0xabc')).rejects.toMatchObject({ code });
|
|
1939
|
+
|
|
1940
|
+
expect(client.getSession()).toBe(session);
|
|
1941
|
+
}
|
|
1942
|
+
expect(session.onEnded).not.toHaveBeenCalled();
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
it('a late rejection of an old session ends it but leaves the new one in use', async () => {
|
|
1946
|
+
client.setSession(session);
|
|
1947
|
+
let respond!: (value: unknown) => void;
|
|
1948
|
+
mockFetch.mockReturnValueOnce(new Promise((resolve) => (respond = resolve)));
|
|
1949
|
+
const pending = client.getState('0xabc');
|
|
1950
|
+
await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledOnce());
|
|
1951
|
+
|
|
1952
|
+
const next = sessionExpiringIn(3600);
|
|
1953
|
+
client.setSession(next);
|
|
1954
|
+
respond({
|
|
1955
|
+
ok: false,
|
|
1956
|
+
headers: new Headers(),
|
|
1957
|
+
status: 401,
|
|
1958
|
+
statusText: 'Unauthorized',
|
|
1959
|
+
text: async () =>
|
|
1960
|
+
JSON.stringify({ code: 'session_revoked', message: 'ended', meta: { retryable: false } }),
|
|
1961
|
+
});
|
|
1962
|
+
await expect(pending).rejects.toMatchObject({ code: 'session_revoked' });
|
|
1963
|
+
|
|
1964
|
+
expect(client.getSession()).toBe(next);
|
|
1965
|
+
expect(session.onEnded).toHaveBeenCalledExactlyOnceWith('revoked');
|
|
1966
|
+
expect(next.onEnded).not.toHaveBeenCalled();
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
it('tells a session it ended only once', async () => {
|
|
1970
|
+
client.setSession(session);
|
|
1971
|
+
rejectWith('session_revoked');
|
|
1972
|
+
rejectWith('session_revoked');
|
|
1973
|
+
|
|
1974
|
+
const results = await Promise.allSettled([client.getState('0xabc'), client.getState('0xabc')]);
|
|
1975
|
+
|
|
1976
|
+
expect(results.map((r) => r.status)).toEqual(['rejected', 'rejected']);
|
|
1977
|
+
expect(session.onEnded).toHaveBeenCalledExactlyOnceWith('revoked');
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
it('falls back to the wallet when the session is about to expire', async () => {
|
|
1981
|
+
const expiring = sessionExpiringIn(10);
|
|
1982
|
+
client.setSession(expiring);
|
|
1983
|
+
expect(client.getSession()).toBeNull();
|
|
1984
|
+
expect(expiring.onEnded).toHaveBeenCalledExactlyOnceWith('expired');
|
|
1985
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => stateBody });
|
|
1986
|
+
|
|
1987
|
+
await client.getState('0xabc');
|
|
1988
|
+
|
|
1989
|
+
expect(lastHeaders()['x-pubkey']).toBe(mockSigner.publicKey);
|
|
1990
|
+
expect(lastHeaders()['x-auth-format']).toBeUndefined();
|
|
1991
|
+
});
|
|
1992
|
+
|
|
1993
|
+
it('posts a session grant in the server shape', async () => {
|
|
1994
|
+
mockFetch.mockResolvedValueOnce({
|
|
1995
|
+
ok: true,
|
|
1996
|
+
json: async () => ({ signer_commitment: '0x11', expires_at: '2026-10-06T18:00:00+00:00' }),
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
const created = await client.createSession({
|
|
2000
|
+
scheme: 'ecdsa',
|
|
2001
|
+
authFormat: 'eip712',
|
|
2002
|
+
publicKey: '0x03' + 'f'.repeat(64),
|
|
2003
|
+
signature: '0x' + '9'.repeat(130),
|
|
2004
|
+
grant: {
|
|
2005
|
+
signerCommitment: '0x11',
|
|
2006
|
+
sessionPublicKey,
|
|
2007
|
+
origin: 'https://app.example',
|
|
2008
|
+
issuedAt: 1791280800,
|
|
2009
|
+
expiresAt: 1791309600,
|
|
2010
|
+
guardianCommitment: '0x22',
|
|
2011
|
+
network: 'devnet',
|
|
2012
|
+
},
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
expect(created).toEqual({ signerCommitment: '0x11', expiresAt: '2026-10-06T18:00:00+00:00' });
|
|
2016
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
2017
|
+
expect(url).toBe('http://localhost:3000/session');
|
|
2018
|
+
expect(init.method).toBe('POST');
|
|
2019
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
2020
|
+
scheme: 'ecdsa',
|
|
2021
|
+
auth_format: 'eip712',
|
|
2022
|
+
public_key: '0x03' + 'f'.repeat(64),
|
|
2023
|
+
signature: '0x' + '9'.repeat(130),
|
|
2024
|
+
grant: {
|
|
2025
|
+
signer_commitment: '0x11',
|
|
2026
|
+
session_public_key: sessionPublicKey,
|
|
2027
|
+
origin: 'https://app.example',
|
|
2028
|
+
issued_at: 1791280800,
|
|
2029
|
+
expires_at: 1791309600,
|
|
2030
|
+
guardian_commitment: '0x22',
|
|
2031
|
+
network: 'devnet',
|
|
2032
|
+
},
|
|
2033
|
+
});
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
it('revokes with a session-key-signed logout and stops using the session', async () => {
|
|
2037
|
+
client.setSession(session);
|
|
2038
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ revoked: true }) });
|
|
2039
|
+
|
|
2040
|
+
await expect(client.revokeSession()).resolves.toBe(true);
|
|
2041
|
+
|
|
2042
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
2043
|
+
expect(url).toBe('http://localhost:3000/session/logout');
|
|
2044
|
+
expect(init.headers).toMatchObject({
|
|
2045
|
+
'x-pubkey': sessionPublicKey,
|
|
2046
|
+
'x-signature': '0x' + 'e'.repeat(128),
|
|
2047
|
+
});
|
|
2048
|
+
const timestamp = Number(init.headers['x-timestamp']);
|
|
2049
|
+
expect(session.signLogout).toHaveBeenCalledWith(timestamp);
|
|
2050
|
+
expect(client.getSession()).toBeNull();
|
|
2051
|
+
expect(session.onEnded).toHaveBeenCalledExactlyOnceWith('logout');
|
|
2052
|
+
await expect(client.revokeSession()).resolves.toBe(false);
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
it('keeps the session when logout fails', async () => {
|
|
2056
|
+
client.setSession(session);
|
|
2057
|
+
rejectWith('storage_error', 500);
|
|
2058
|
+
|
|
2059
|
+
await expect(client.revokeSession()).rejects.toMatchObject({ code: 'storage_error' });
|
|
2060
|
+
|
|
2061
|
+
expect(client.getSession()).toBe(session);
|
|
2062
|
+
expect(session.onEnded).not.toHaveBeenCalled();
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
it('logs out a given session without touching the current one', async () => {
|
|
2066
|
+
const old = sessionExpiringIn(3600);
|
|
2067
|
+
client.setSession(session);
|
|
2068
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ revoked: true }) });
|
|
2069
|
+
|
|
2070
|
+
await expect(client.revokeSession(old)).resolves.toBe(true);
|
|
2071
|
+
|
|
2072
|
+
expect(old.signLogout).toHaveBeenCalledOnce();
|
|
2073
|
+
expect(client.getSession()).toBe(session);
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
it('maps advertised session parameters from GET /status', async () => {
|
|
2077
|
+
mockFetch.mockResolvedValueOnce({
|
|
2078
|
+
ok: true,
|
|
2079
|
+
json: async () => ({
|
|
2080
|
+
status: 'ok',
|
|
2081
|
+
version: '0.18.0',
|
|
2082
|
+
git_commit: 'abc',
|
|
2083
|
+
environment: 'devnet',
|
|
2084
|
+
started_at: '2026-10-06T00:00:00Z',
|
|
2085
|
+
uptime_seconds: 1,
|
|
2086
|
+
sessions: { max_ttl_seconds: 28800 },
|
|
2087
|
+
}),
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
const status = await client.getStatus();
|
|
2091
|
+
|
|
2092
|
+
expect(status.sessions).toEqual({ maxTtlSeconds: 28800 });
|
|
2093
|
+
});
|
|
2094
|
+
});
|