@openzeppelin/guardian-client 0.19.0-rc.1 → 0.19.0-rc.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 +61 -4
- package/dist/conversion.d.ts +8 -2
- package/dist/conversion.d.ts.map +1 -1
- package/dist/conversion.js +34 -1
- 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 +2 -0
- package/dist/error-codes.js.map +1 -1
- package/dist/http.d.ts +24 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +74 -20
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/request-envelope.d.ts +2 -2
- package/dist/request-envelope.d.ts.map +1 -1
- package/dist/request-envelope.js +2 -2
- package/dist/request-envelope.js.map +1 -1
- package/dist/server-types.d.ts +13 -0
- package/dist/server-types.d.ts.map +1 -1
- package/dist/session-request.d.ts +104 -0
- package/dist/session-request.d.ts.map +1 -0
- package/dist/session-request.js +164 -0
- package/dist/session-request.js.map +1 -0
- package/dist/types.d.ts +76 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/conversion.test.ts +17 -0
- package/src/conversion.ts +42 -1
- package/src/error-codes.ts +2 -0
- package/src/http.test.ts +328 -11
- package/src/http.ts +97 -23
- package/src/index.ts +16 -0
- package/src/request-envelope.test.ts +4 -4
- package/src/request-envelope.ts +2 -2
- package/src/server-types.ts +15 -0
- package/src/session-request.test.ts +178 -0
- package/src/session-request.ts +191 -0
- package/src/types.ts +79 -3
package/src/http.test.ts
CHANGED
|
@@ -2,6 +2,12 @@ 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
4
|
import { RequestAuthPayload } from './auth-request.js';
|
|
5
|
+
import {
|
|
6
|
+
SessionOperation,
|
|
7
|
+
SessionTimestamps,
|
|
8
|
+
UnsupportedAuthVersionError,
|
|
9
|
+
WriteSequencer,
|
|
10
|
+
} from './session-request.js';
|
|
5
11
|
import type { Signer, ConfigureResponse, StateObject, DeltaObject, DeltaProposalResponse, SessionRequestSigner } from './types.js';
|
|
6
12
|
|
|
7
13
|
// Mock fetch globally
|
|
@@ -443,6 +449,143 @@ describe('GuardianHttpClient', () => {
|
|
|
443
449
|
});
|
|
444
450
|
});
|
|
445
451
|
|
|
452
|
+
describe('getStateTail', () => {
|
|
453
|
+
const accountId = '0x' + 'a'.repeat(30);
|
|
454
|
+
const serverState = {
|
|
455
|
+
account_id: accountId,
|
|
456
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
457
|
+
state_json: { data: 'tail' },
|
|
458
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
459
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
460
|
+
auth_scheme: 'falcon',
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
it('reads the tail and its queue, signed over the account query', async () => {
|
|
464
|
+
client.setSigner(mockSigner);
|
|
465
|
+
mockFetch.mockResolvedValueOnce({
|
|
466
|
+
ok: true,
|
|
467
|
+
json: async () => ({
|
|
468
|
+
state: serverState,
|
|
469
|
+
queue: [
|
|
470
|
+
{
|
|
471
|
+
nonce: 5,
|
|
472
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
473
|
+
new_commitment: '0x' + 'c'.repeat(64),
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
accepts_submissions: false,
|
|
477
|
+
}),
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const tail = await client.getStateTail(accountId);
|
|
481
|
+
|
|
482
|
+
expect(tail).toEqual({
|
|
483
|
+
state: {
|
|
484
|
+
accountId,
|
|
485
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
486
|
+
stateJson: { data: 'tail' },
|
|
487
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
488
|
+
updatedAt: '2024-01-02T00:00:00Z',
|
|
489
|
+
authScheme: 'falcon',
|
|
490
|
+
},
|
|
491
|
+
queue: [
|
|
492
|
+
{
|
|
493
|
+
nonce: 5,
|
|
494
|
+
prevCommitment: '0x' + 'b'.repeat(64),
|
|
495
|
+
newCommitment: '0x' + 'c'.repeat(64),
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
acceptsSubmissions: false,
|
|
499
|
+
});
|
|
500
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
501
|
+
`http://localhost:3000/state/tail?account_id=${accountId}`,
|
|
502
|
+
expect.objectContaining({ method: 'GET' })
|
|
503
|
+
);
|
|
504
|
+
expect(mockSigner.signRequest).toHaveBeenCalledWith(
|
|
505
|
+
accountId,
|
|
506
|
+
expect.any(Number),
|
|
507
|
+
expect.objectContaining({ canonicalJson: JSON.stringify({ account_id: accountId }), tag: '' }),
|
|
508
|
+
);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('reads an empty queue as the canonical state', async () => {
|
|
512
|
+
client.setSigner(mockSigner);
|
|
513
|
+
mockFetch.mockResolvedValueOnce({
|
|
514
|
+
ok: true,
|
|
515
|
+
json: async () => ({ state: serverState, queue: [], accepts_submissions: true }),
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const tail = await client.getStateTail(accountId);
|
|
519
|
+
|
|
520
|
+
expect(tail.queue).toEqual([]);
|
|
521
|
+
expect(tail.state.commitment).toBe('0x' + 'c'.repeat(64));
|
|
522
|
+
expect(tail.acceptsSubmissions).toBe(true);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('rejects a tail that does not say whether it accepts submissions', async () => {
|
|
526
|
+
client.setSigner(mockSigner);
|
|
527
|
+
mockFetch.mockResolvedValueOnce({
|
|
528
|
+
ok: true,
|
|
529
|
+
json: async () => ({ state: serverState, queue: [] }),
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
await expect(client.getStateTail(accountId)).rejects.toThrow(
|
|
533
|
+
'Invalid accepts_submissions from server'
|
|
534
|
+
);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('surfaces an older server without the route as a 404', async () => {
|
|
538
|
+
client.setSigner(mockSigner);
|
|
539
|
+
mockFetch.mockResolvedValueOnce({
|
|
540
|
+
ok: false,
|
|
541
|
+
headers: new Headers(),
|
|
542
|
+
status: 404,
|
|
543
|
+
statusText: 'Not Found',
|
|
544
|
+
text: async () => '',
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const error = await client.getStateTail(accountId).catch((e) => e);
|
|
548
|
+
expect(error).toBeInstanceOf(GuardianHttpError);
|
|
549
|
+
expect(error.status).toBe(404);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('surfaces a queue with no tail to serve as a pending conflict', async () => {
|
|
553
|
+
client.setSigner(mockSigner);
|
|
554
|
+
mockFetch.mockResolvedValueOnce({
|
|
555
|
+
ok: false,
|
|
556
|
+
headers: new Headers(),
|
|
557
|
+
status: 409,
|
|
558
|
+
statusText: 'Conflict',
|
|
559
|
+
text: async () =>
|
|
560
|
+
JSON.stringify({
|
|
561
|
+
code: 'conflict_pending_delta',
|
|
562
|
+
message: 'A delta is already pending for this account',
|
|
563
|
+
meta: { retryable: true },
|
|
564
|
+
}),
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const error = await client.getStateTail(accountId).catch((e) => e);
|
|
568
|
+
expect(error).toBeInstanceOf(GuardianHttpError);
|
|
569
|
+
expect(error.code).toBe('conflict_pending_delta');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('rejects a queued nonce the client cannot represent', async () => {
|
|
573
|
+
client.setSigner(mockSigner);
|
|
574
|
+
mockFetch.mockResolvedValueOnce({
|
|
575
|
+
ok: true,
|
|
576
|
+
json: async () => ({
|
|
577
|
+
state: serverState,
|
|
578
|
+
queue: [{ nonce: 2 ** 53, prev_commitment: '0x1', new_commitment: '0x2' }],
|
|
579
|
+
accepts_submissions: false,
|
|
580
|
+
}),
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await expect(client.getStateTail(accountId)).rejects.toThrow(
|
|
584
|
+
'Invalid queued candidate nonce'
|
|
585
|
+
);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
|
|
446
589
|
describe('getState', () => {
|
|
447
590
|
it('uses the existing signed headers with an EIP-712 format selector', async () => {
|
|
448
591
|
const signer: Signer = {
|
|
@@ -1993,8 +2136,13 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
1993
2136
|
let session: SessionRequestSigner;
|
|
1994
2137
|
|
|
1995
2138
|
function sessionExpiringIn(seconds: number): SessionRequestSigner {
|
|
2139
|
+
const timestamps = new SessionTimestamps();
|
|
1996
2140
|
return {
|
|
1997
2141
|
publicKey: sessionPublicKey,
|
|
2142
|
+
nextTimestamp: vi.fn((accountId: string, afterReplay: boolean) =>
|
|
2143
|
+
timestamps.next(accountId, afterReplay)
|
|
2144
|
+
),
|
|
2145
|
+
writeSequencer: new WriteSequencer(),
|
|
1998
2146
|
signerCommitment: mockSigner.commitment,
|
|
1999
2147
|
expiresAt: Math.floor(Date.now() / 1000) + seconds,
|
|
2000
2148
|
signRequest: vi.fn().mockReturnValue('0x' + 'd'.repeat(128)),
|
|
@@ -2034,6 +2182,7 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
2034
2182
|
const calls: Array<[string, () => Promise<unknown>]> = [
|
|
2035
2183
|
['getState', () => client.getState(accountId)],
|
|
2036
2184
|
['getCanonicalNonce', () => client.getCanonicalNonce(accountId)],
|
|
2185
|
+
['getStateTail', () => client.getStateTail(accountId)],
|
|
2037
2186
|
['getDelta', () => client.getDelta(accountId, 1)],
|
|
2038
2187
|
['getDeltaSince', () => client.getDeltaSince(accountId, 1)],
|
|
2039
2188
|
['getDeltaHistory', () => client.getDeltaHistory(accountId)],
|
|
@@ -2065,10 +2214,26 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
2065
2214
|
// Only the request headers matter; the empty body may fail to parse.
|
|
2066
2215
|
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
|
|
2067
2216
|
await call().catch(() => undefined);
|
|
2068
|
-
expect(lastHeaders()['x-auth-format'], name).toBe('session');
|
|
2217
|
+
expect(lastHeaders()['x-auth-format'], name).toBe('session-v2');
|
|
2069
2218
|
expect(lastHeaders()['x-pubkey'], name).toBe(sessionPublicKey);
|
|
2070
2219
|
}
|
|
2071
2220
|
expect(session.signRequest).toHaveBeenCalledTimes(calls.length);
|
|
2221
|
+
expect(session.nextTimestamp).toHaveBeenCalledTimes(calls.length);
|
|
2222
|
+
expect(vi.mocked(session.nextTimestamp).mock.calls.every(([id]) => id === accountId)).toBe(true);
|
|
2223
|
+
expect(vi.mocked(session.signRequest).mock.calls.map(([operation]) => operation)).toEqual([
|
|
2224
|
+
SessionOperation.GetState,
|
|
2225
|
+
SessionOperation.GetCanonicalNonce,
|
|
2226
|
+
SessionOperation.GetStateTail,
|
|
2227
|
+
SessionOperation.GetDelta,
|
|
2228
|
+
SessionOperation.GetDeltaSince,
|
|
2229
|
+
SessionOperation.GetDeltaHistory,
|
|
2230
|
+
SessionOperation.GetDeltaProposals,
|
|
2231
|
+
SessionOperation.GetDeltaProposal,
|
|
2232
|
+
SessionOperation.GetDeltaProposalExecution,
|
|
2233
|
+
SessionOperation.GetCurrentExecution,
|
|
2234
|
+
SessionOperation.PushDeltaProposal,
|
|
2235
|
+
SessionOperation.SignDeltaProposal,
|
|
2236
|
+
]);
|
|
2072
2237
|
expect(mockSigner.signRequest).not.toHaveBeenCalled();
|
|
2073
2238
|
});
|
|
2074
2239
|
|
|
@@ -2098,7 +2263,7 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
2098
2263
|
|
|
2099
2264
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
2100
2265
|
const [first, second] = mockFetch.mock.calls.map(([, init]) => init.headers);
|
|
2101
|
-
expect(second['x-auth-format']).toBe('session');
|
|
2266
|
+
expect(second['x-auth-format']).toBe('session-v2');
|
|
2102
2267
|
expect(second['x-timestamp']).not.toBe(first['x-timestamp']);
|
|
2103
2268
|
expect(session.signRequest).toHaveBeenCalledTimes(2);
|
|
2104
2269
|
expect(client.getSession()).toBe(session);
|
|
@@ -2127,7 +2292,7 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
2127
2292
|
|
|
2128
2293
|
expect(result.ackSig).toBe('0x' + 'f'.repeat(128));
|
|
2129
2294
|
const [first, second] = mockFetch.mock.calls.map(([, init]) => init.headers);
|
|
2130
|
-
expect(first['x-auth-format']).toBe('session');
|
|
2295
|
+
expect(first['x-auth-format']).toBe('session-v2');
|
|
2131
2296
|
expect(second['x-pubkey']).toBe(mockSigner.publicKey);
|
|
2132
2297
|
expect(client.getSession()).toBe(session);
|
|
2133
2298
|
expect(session.onEnded).not.toHaveBeenCalled();
|
|
@@ -2154,7 +2319,7 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
2154
2319
|
expect(lastHeaders()).toMatchObject({
|
|
2155
2320
|
'x-pubkey': sessionPublicKey,
|
|
2156
2321
|
'x-signature': '0x' + 'd'.repeat(128),
|
|
2157
|
-
'x-auth-format': 'session',
|
|
2322
|
+
'x-auth-format': 'session-v2',
|
|
2158
2323
|
});
|
|
2159
2324
|
expect(session.signRequest).toHaveBeenCalledOnce();
|
|
2160
2325
|
expect(mockSigner.signRequest).not.toHaveBeenCalled();
|
|
@@ -2195,8 +2360,11 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
2195
2360
|
.mockResolvedValueOnce({ ok: true, json: async () => ({ execution }) });
|
|
2196
2361
|
|
|
2197
2362
|
await client.executeDeltaProposal('0xabc', '0xproposal');
|
|
2198
|
-
expect(lastHeaders()['x-auth-format']).toBe('session');
|
|
2199
|
-
expect(vi.mocked(session.signRequest).mock.calls[0][
|
|
2363
|
+
expect(lastHeaders()['x-auth-format']).toBe('session-v2');
|
|
2364
|
+
expect(vi.mocked(session.signRequest).mock.calls[0][0]).toBe(
|
|
2365
|
+
SessionOperation.ExecuteDeltaProposal
|
|
2366
|
+
);
|
|
2367
|
+
expect(vi.mocked(session.signRequest).mock.calls[0][3].toBytes()).toEqual(
|
|
2200
2368
|
RequestAuthPayload.forOperation('ExecuteDeltaProposal', {
|
|
2201
2369
|
account_id: '0xabc',
|
|
2202
2370
|
proposal_id: '0xproposal',
|
|
@@ -2204,9 +2372,9 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
2204
2372
|
);
|
|
2205
2373
|
|
|
2206
2374
|
await client.getDeltaProposalExecution('0xabc', '0xproposal');
|
|
2207
|
-
expect(lastHeaders()['x-auth-format']).toBe('session');
|
|
2375
|
+
expect(lastHeaders()['x-auth-format']).toBe('session-v2');
|
|
2208
2376
|
await client.getCurrentExecution('0xabc');
|
|
2209
|
-
expect(lastHeaders()['x-auth-format']).toBe('session');
|
|
2377
|
+
expect(lastHeaders()['x-auth-format']).toBe('session-v2');
|
|
2210
2378
|
|
|
2211
2379
|
expect(mockSigner.signRequest).not.toHaveBeenCalled();
|
|
2212
2380
|
expect(session.signRequest).toHaveBeenCalledTimes(3);
|
|
@@ -2240,7 +2408,7 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
2240
2408
|
});
|
|
2241
2409
|
expect(lastHeaders()).toMatchObject({
|
|
2242
2410
|
'x-pubkey': sessionPublicKey,
|
|
2243
|
-
'x-auth-format': 'session',
|
|
2411
|
+
'x-auth-format': 'session-v2',
|
|
2244
2412
|
});
|
|
2245
2413
|
expect(session.signRequest).toHaveBeenCalledOnce();
|
|
2246
2414
|
|
|
@@ -2357,6 +2525,18 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
2357
2525
|
expect(session.onEnded).not.toHaveBeenCalled();
|
|
2358
2526
|
});
|
|
2359
2527
|
|
|
2528
|
+
it('keeps the session when the device clock is outside the timestamp window', async () => {
|
|
2529
|
+
client.setSession(session);
|
|
2530
|
+
rejectWith('timestamp_out_of_window');
|
|
2531
|
+
|
|
2532
|
+
await expect(client.getState('0xabc')).rejects.toMatchObject({
|
|
2533
|
+
code: 'timestamp_out_of_window',
|
|
2534
|
+
});
|
|
2535
|
+
expect(client.getSession()).toBe(session);
|
|
2536
|
+
expect(session.onEnded).not.toHaveBeenCalled();
|
|
2537
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
2538
|
+
});
|
|
2539
|
+
|
|
2360
2540
|
it('a late rejection of an old session ends it but leaves the new one in use', async () => {
|
|
2361
2541
|
client.setSession(session);
|
|
2362
2542
|
let respond!: (value: unknown) => void;
|
|
@@ -2405,10 +2585,143 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
2405
2585
|
expect(lastHeaders()['x-auth-format']).toBeUndefined();
|
|
2406
2586
|
});
|
|
2407
2587
|
|
|
2588
|
+
it('sends a session write to an account only after the previous one finished', async () => {
|
|
2589
|
+
client.setSession(session);
|
|
2590
|
+
let releaseFirst!: () => void;
|
|
2591
|
+
const delta = {
|
|
2592
|
+
account_id: '0xabc',
|
|
2593
|
+
nonce: 1,
|
|
2594
|
+
status: { status: 'pending', timestamp: '2026-01-01T00:00:00Z', proposer_id: '0x1', cosigner_sigs: [] },
|
|
2595
|
+
delta_payload: {},
|
|
2596
|
+
prev_commitment: '0x00',
|
|
2597
|
+
};
|
|
2598
|
+
mockFetch
|
|
2599
|
+
.mockImplementationOnce(
|
|
2600
|
+
() =>
|
|
2601
|
+
new Promise((resolve) => {
|
|
2602
|
+
releaseFirst = () => resolve({ ok: true, json: async () => ({ delta, commitment: '0x1' }) });
|
|
2603
|
+
})
|
|
2604
|
+
)
|
|
2605
|
+
.mockResolvedValueOnce({ ok: true, json: async () => ({ delta, commitment: '0x2' }) });
|
|
2606
|
+
const proposal = { accountId: '0xabc', nonce: 1, deltaPayload: { txSummary: { data: 'AAAA' }, signatures: [] } };
|
|
2607
|
+
|
|
2608
|
+
const first = client.pushDeltaProposal(proposal).catch(() => undefined);
|
|
2609
|
+
const second = client.pushDeltaProposal(proposal).catch(() => undefined);
|
|
2610
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
2611
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
2612
|
+
|
|
2613
|
+
releaseFirst();
|
|
2614
|
+
await Promise.all([first, second]);
|
|
2615
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
2616
|
+
const [firstTimestamp, secondTimestamp] = mockFetch.mock.calls.map(
|
|
2617
|
+
([, init]) => Number(init.headers['x-timestamp'])
|
|
2618
|
+
);
|
|
2619
|
+
expect(secondTimestamp).toBeGreaterThan(firstTimestamp);
|
|
2620
|
+
});
|
|
2621
|
+
|
|
2622
|
+
it('moves a write queued on a replaced session into the new session\'s turn', async () => {
|
|
2623
|
+
const replacement = { ...sessionExpiringIn(3600), publicKey: '0x03' + 'f'.repeat(64) };
|
|
2624
|
+
client.setSession(session);
|
|
2625
|
+
const delta = {
|
|
2626
|
+
account_id: '0xabc',
|
|
2627
|
+
nonce: 1,
|
|
2628
|
+
status: { status: 'pending', timestamp: '2026-01-01T00:00:00Z', proposer_id: '0x1', cosigner_sigs: [] },
|
|
2629
|
+
delta_payload: {},
|
|
2630
|
+
prev_commitment: '0x00',
|
|
2631
|
+
};
|
|
2632
|
+
const done = { ok: true, json: async () => ({ delta, commitment: '0x1' }) };
|
|
2633
|
+
const releases: Array<() => void> = [];
|
|
2634
|
+
mockFetch.mockImplementation(
|
|
2635
|
+
() => new Promise((resolve) => releases.push(() => resolve(done)))
|
|
2636
|
+
);
|
|
2637
|
+
const proposal = { accountId: '0xabc', nonce: 1, deltaPayload: { txSummary: { data: 'AAAA' }, signatures: [] } };
|
|
2638
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 20));
|
|
2639
|
+
|
|
2640
|
+
const first = client.pushDeltaProposal(proposal).catch(() => undefined);
|
|
2641
|
+
const queued = client.pushDeltaProposal(proposal).catch(() => undefined);
|
|
2642
|
+
await settle();
|
|
2643
|
+
client.setSession(replacement);
|
|
2644
|
+
const onReplacement = client.pushDeltaProposal(proposal).catch(() => undefined);
|
|
2645
|
+
await settle();
|
|
2646
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
2647
|
+
|
|
2648
|
+
releases[0]();
|
|
2649
|
+
await settle();
|
|
2650
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
2651
|
+
|
|
2652
|
+
releases[1]();
|
|
2653
|
+
await settle();
|
|
2654
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
2655
|
+
releases[2]();
|
|
2656
|
+
await Promise.all([first, queued, onReplacement]);
|
|
2657
|
+
expect(mockFetch.mock.calls.map(([, init]) => init.headers['x-pubkey'])).toEqual([
|
|
2658
|
+
sessionPublicKey,
|
|
2659
|
+
replacement.publicKey,
|
|
2660
|
+
replacement.publicKey,
|
|
2661
|
+
]);
|
|
2662
|
+
expect(session.signRequest).toHaveBeenCalledTimes(1);
|
|
2663
|
+
mockFetch.mockReset();
|
|
2664
|
+
});
|
|
2665
|
+
|
|
2666
|
+
it('does not order session reads', async () => {
|
|
2667
|
+
client.setSession(session);
|
|
2668
|
+
mockFetch.mockImplementation(() => new Promise(() => undefined));
|
|
2669
|
+
void client.getState('0xabc');
|
|
2670
|
+
void client.getState('0xabc');
|
|
2671
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
2672
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
2673
|
+
mockFetch.mockReset();
|
|
2674
|
+
});
|
|
2675
|
+
|
|
2676
|
+
it('refuses a Guardian that does not report session request version 2', async () => {
|
|
2677
|
+
for (const body of [
|
|
2678
|
+
{ signer_commitment: '0x11', expires_at: '2026-10-06T18:00:00+00:00' },
|
|
2679
|
+
{
|
|
2680
|
+
signer_commitment: '0x11',
|
|
2681
|
+
expires_at: '2026-10-06T18:00:00+00:00',
|
|
2682
|
+
request_auth_version: 3,
|
|
2683
|
+
},
|
|
2684
|
+
]) {
|
|
2685
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => body });
|
|
2686
|
+
await expect(
|
|
2687
|
+
client.createSession({
|
|
2688
|
+
scheme: 'falcon',
|
|
2689
|
+
signature: '0x00',
|
|
2690
|
+
grant: {
|
|
2691
|
+
signerCommitment: '0x11',
|
|
2692
|
+
sessionPublicKey: sessionPublicKey,
|
|
2693
|
+
origin: '',
|
|
2694
|
+
issuedAt: 0,
|
|
2695
|
+
expiresAt: 1,
|
|
2696
|
+
guardianCommitment: '0x22',
|
|
2697
|
+
network: 'devnet',
|
|
2698
|
+
},
|
|
2699
|
+
})
|
|
2700
|
+
).rejects.toBeInstanceOf(UnsupportedAuthVersionError);
|
|
2701
|
+
}
|
|
2702
|
+
});
|
|
2703
|
+
|
|
2704
|
+
it('keeps the session and does not retry when Guardian refuses its request format', async () => {
|
|
2705
|
+
client.setSession(session);
|
|
2706
|
+
rejectWith('unsupported_auth_version', 400);
|
|
2707
|
+
|
|
2708
|
+
const error = await client.getState('0xabc').catch((err: unknown) => err);
|
|
2709
|
+
|
|
2710
|
+
expect(error).toBeInstanceOf(GuardianHttpError);
|
|
2711
|
+
expect((error as GuardianHttpError).code).toBe('unsupported_auth_version');
|
|
2712
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
2713
|
+
expect(client.getSession()).toBe(session);
|
|
2714
|
+
expect(session.onEnded).not.toHaveBeenCalled();
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2408
2717
|
it('posts a session grant in the server shape', async () => {
|
|
2409
2718
|
mockFetch.mockResolvedValueOnce({
|
|
2410
2719
|
ok: true,
|
|
2411
|
-
json: async () => ({
|
|
2720
|
+
json: async () => ({
|
|
2721
|
+
signer_commitment: '0x11',
|
|
2722
|
+
expires_at: '2026-10-06T18:00:00+00:00',
|
|
2723
|
+
request_auth_version: 2,
|
|
2724
|
+
}),
|
|
2412
2725
|
});
|
|
2413
2726
|
|
|
2414
2727
|
const created = await client.createSession({
|
|
@@ -2427,7 +2740,11 @@ describe('GuardianHttpClient sessions', () => {
|
|
|
2427
2740
|
},
|
|
2428
2741
|
});
|
|
2429
2742
|
|
|
2430
|
-
expect(created).toEqual({
|
|
2743
|
+
expect(created).toEqual({
|
|
2744
|
+
signerCommitment: '0x11',
|
|
2745
|
+
expiresAt: '2026-10-06T18:00:00+00:00',
|
|
2746
|
+
requestAuthVersion: 2,
|
|
2747
|
+
});
|
|
2431
2748
|
const [url, init] = mockFetch.mock.calls[0];
|
|
2432
2749
|
expect(url).toBe('http://localhost:3000/session');
|
|
2433
2750
|
expect(init.method).toBe('POST');
|