@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.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
AbandonStatus,
|
|
8
8
|
ConfigureRequest,
|
|
9
9
|
ConfigureResponse,
|
|
10
|
+
CreateSessionRequest,
|
|
11
|
+
CreateSessionResponse,
|
|
10
12
|
DeltaObject,
|
|
11
13
|
DeltaProposalRequest,
|
|
12
14
|
DeltaProposalResponse,
|
|
@@ -16,6 +18,8 @@ import type {
|
|
|
16
18
|
LookupResponse,
|
|
17
19
|
PubkeyResponse,
|
|
18
20
|
PushDeltaResponse,
|
|
21
|
+
SessionEndReason,
|
|
22
|
+
SessionRequestSigner,
|
|
19
23
|
SignProposalRequest,
|
|
20
24
|
SignatureScheme,
|
|
21
25
|
Signer,
|
|
@@ -36,17 +40,23 @@ import type {
|
|
|
36
40
|
ServerPubkeyResponse,
|
|
37
41
|
ServerStateObject,
|
|
38
42
|
ServerConfigureResponse,
|
|
43
|
+
ServerCreateSessionResponse,
|
|
39
44
|
ServerPushDeltaResponse,
|
|
45
|
+
ServerRevokeAllSessionsRequest,
|
|
46
|
+
ServerRevokeAllSessionsResponse,
|
|
47
|
+
ServerRevokeSessionResponse,
|
|
40
48
|
ServerStatusResponse,
|
|
41
49
|
} from './server-types.js';
|
|
42
50
|
import {
|
|
43
51
|
fromServerCanonicalNonce,
|
|
44
52
|
fromServerConfigureResponse,
|
|
53
|
+
fromServerCreateSessionResponse,
|
|
45
54
|
fromServerDeltaObject,
|
|
46
55
|
fromServerHistoryPage,
|
|
47
56
|
fromServerLookupResponse,
|
|
48
57
|
fromServerStateObject,
|
|
49
58
|
toServerConfigureRequest,
|
|
59
|
+
toServerCreateSessionRequest,
|
|
50
60
|
toServerDeltaProposalRequest,
|
|
51
61
|
toServerExecutionDelta,
|
|
52
62
|
toServerSignProposalRequest,
|
|
@@ -226,11 +236,46 @@ function parseRetryAfterSeconds(header: string | null | undefined): number | und
|
|
|
226
236
|
return Number.isSafeInteger(secs) ? secs : undefined;
|
|
227
237
|
}
|
|
228
238
|
|
|
239
|
+
/**
|
|
240
|
+
* A session is set aside this long before it expires, so a request never
|
|
241
|
+
* reaches the server already expired; requests then fall back to the wallet.
|
|
242
|
+
*/
|
|
243
|
+
const SESSION_EXPIRY_MARGIN_MS = 30_000;
|
|
244
|
+
|
|
245
|
+
/** Codes that end a session-signed request's session for good. */
|
|
246
|
+
const SESSION_END_REASONS: Record<string, SessionEndReason> = {
|
|
247
|
+
session_expired: 'expired',
|
|
248
|
+
session_revoked: 'revoked',
|
|
249
|
+
authentication_failed: 'rejected',
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Which key signs a per-account request. Mirrors the server: requests are
|
|
254
|
+
* wallet-signed unless the route is session-eligible.
|
|
255
|
+
*/
|
|
256
|
+
type AuthMode = 'session' | 'wallet';
|
|
257
|
+
|
|
258
|
+
function authHeaders(
|
|
259
|
+
publicKey: string,
|
|
260
|
+
signature: string,
|
|
261
|
+
timestamp: number,
|
|
262
|
+
format?: 'eip712' | 'session'
|
|
263
|
+
): Record<string, string> {
|
|
264
|
+
return {
|
|
265
|
+
'x-pubkey': publicKey,
|
|
266
|
+
'x-signature': signature,
|
|
267
|
+
'x-timestamp': timestamp.toString(),
|
|
268
|
+
...(format ? { 'x-auth-format': format } : {}),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
229
272
|
/**
|
|
230
273
|
* Minimal HTTP client for GUARDIAN server.
|
|
231
274
|
*/
|
|
232
275
|
export class GuardianHttpClient {
|
|
233
276
|
private signer: Signer | null = null;
|
|
277
|
+
private session: SessionRequestSigner | null = null;
|
|
278
|
+
private readonly endedSessions = new WeakSet<SessionRequestSigner>();
|
|
234
279
|
private readonly baseUrl: string;
|
|
235
280
|
private lastTimestamp = 0;
|
|
236
281
|
|
|
@@ -254,6 +299,121 @@ export class GuardianHttpClient {
|
|
|
254
299
|
this.signer = signer;
|
|
255
300
|
}
|
|
256
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Sign session-eligible requests (reads, proposal list/get, proposal
|
|
304
|
+
* create/sign) with a registered delegated signer instead of the wallet.
|
|
305
|
+
* Every other route, including `configure`, `pushDelta`,
|
|
306
|
+
* `abandonCandidate`, account lookup and `revokeAllSessions`, uses the
|
|
307
|
+
* wallet signer. Pass `null` to stop using the session.
|
|
308
|
+
*/
|
|
309
|
+
setSession(session: SessionRequestSigner | null): void {
|
|
310
|
+
this.session = session;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The session in use, or `null` when none is set, no wallet signer is set,
|
|
315
|
+
* the session was granted by another wallet than the current signer, or it
|
|
316
|
+
* is about to expire. A session reaching its expiry margin is dropped and
|
|
317
|
+
* told so.
|
|
318
|
+
*/
|
|
319
|
+
getSession(): SessionRequestSigner | null {
|
|
320
|
+
const session = this.session;
|
|
321
|
+
if (!session) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
if (session.expiresAt * 1000 - SESSION_EXPIRY_MARGIN_MS <= Date.now()) {
|
|
325
|
+
this.endSession(session, 'expired');
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
if (
|
|
329
|
+
!this.signer ||
|
|
330
|
+
this.signer.commitment.toLowerCase() !== session.signerCommitment.toLowerCase()
|
|
331
|
+
) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
return session;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Stops using `session` if it is still the current one, and tells it why
|
|
339
|
+
* once. A newer session set meanwhile stays in use.
|
|
340
|
+
*/
|
|
341
|
+
private endSession(session: SessionRequestSigner, reason: SessionEndReason): void {
|
|
342
|
+
if (this.session === session) {
|
|
343
|
+
this.session = null;
|
|
344
|
+
}
|
|
345
|
+
if (!this.endedSessions.has(session)) {
|
|
346
|
+
this.endedSessions.add(session);
|
|
347
|
+
session.onEnded?.(reason);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Register a wallet-signed session grant (`POST /session`). */
|
|
352
|
+
async createSession(request: CreateSessionRequest): Promise<CreateSessionResponse> {
|
|
353
|
+
const response = await this.fetch('/session', {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
body: JSON.stringify(toServerCreateSessionRequest(request)),
|
|
356
|
+
});
|
|
357
|
+
return fromServerCreateSessionResponse((await response.json()) as ServerCreateSessionResponse);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Revoke a session on the server (`POST /session/logout`), the current one
|
|
362
|
+
* by default, and stop using it. Returns whether the server still had it
|
|
363
|
+
* active; `false` when there is none. On error the session stays in use.
|
|
364
|
+
*/
|
|
365
|
+
async revokeSession(session: SessionRequestSigner | null = this.session): Promise<boolean> {
|
|
366
|
+
if (!session) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
const timestamp = this.nextTimestamp();
|
|
370
|
+
const signature = await session.signLogout(timestamp);
|
|
371
|
+
const response = await this.fetch('/session/logout', {
|
|
372
|
+
method: 'POST',
|
|
373
|
+
headers: authHeaders(session.publicKey, signature, timestamp),
|
|
374
|
+
});
|
|
375
|
+
const data = (await response.json()) as ServerRevokeSessionResponse;
|
|
376
|
+
this.endSession(session, 'logout');
|
|
377
|
+
return data.revoked;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Revoke every session of the wallet signer on the server
|
|
382
|
+
* (`POST /session/revoke-all`), including sessions started elsewhere, and
|
|
383
|
+
* stop using the session that was current when the call was made. A
|
|
384
|
+
* session started while the call is in flight was issued after it and
|
|
385
|
+
* stays in use. Signed by the wallet. Returns how many sessions the server
|
|
386
|
+
* revoked.
|
|
387
|
+
*/
|
|
388
|
+
async revokeAllSessions(): Promise<number> {
|
|
389
|
+
const signer = this.signer;
|
|
390
|
+
if (!signer) {
|
|
391
|
+
throw new Error('No signer configured. Call setSigner() first.');
|
|
392
|
+
}
|
|
393
|
+
if (!signer.signSessionRevokeAll) {
|
|
394
|
+
throw new Error('This signer cannot sign Guardian session revocations');
|
|
395
|
+
}
|
|
396
|
+
const revoking = this.session;
|
|
397
|
+
const timestamp = this.nextTimestamp();
|
|
398
|
+
const signature = await signer.signSessionRevokeAll(signer.commitment, timestamp);
|
|
399
|
+
const body: ServerRevokeAllSessionsRequest = { signer_commitment: signer.commitment };
|
|
400
|
+
const response = await this.fetch('/session/revoke-all', {
|
|
401
|
+
method: 'POST',
|
|
402
|
+
headers: authHeaders(signer.publicKey, signature, timestamp, signer.requestAuthFormat),
|
|
403
|
+
body: JSON.stringify(body),
|
|
404
|
+
});
|
|
405
|
+
const data = (await response.json()) as ServerRevokeAllSessionsResponse;
|
|
406
|
+
if (revoking?.signerCommitment.toLowerCase() === signer.commitment.toLowerCase()) {
|
|
407
|
+
this.endSession(revoking, 'logout');
|
|
408
|
+
}
|
|
409
|
+
return data.revoked;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Guardian's ACK commitment for one signature scheme (`GET /pubkey`).
|
|
414
|
+
* Omitting `scheme` returns the Falcon key even on a server that admits only
|
|
415
|
+
* ECDSA accounts, so pass the scheme of the account the commitment is for.
|
|
416
|
+
*/
|
|
257
417
|
async getPubkey(scheme?: SignatureScheme): Promise<PubkeyResponse> {
|
|
258
418
|
const query = scheme ? `?scheme=${scheme}` : '';
|
|
259
419
|
const response = await this.fetch(`/pubkey${query}`, { method: 'GET' });
|
|
@@ -274,11 +434,19 @@ export class GuardianHttpClient {
|
|
|
274
434
|
environment: data.environment,
|
|
275
435
|
startedAt: data.started_at,
|
|
276
436
|
uptimeSeconds: data.uptime_seconds,
|
|
437
|
+
...(data.sessions
|
|
438
|
+
? {
|
|
439
|
+
sessions: {
|
|
440
|
+
maxTtlSeconds: data.sessions.max_ttl_seconds,
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
: {}),
|
|
277
444
|
};
|
|
278
445
|
}
|
|
279
446
|
|
|
280
447
|
async configure(request: ConfigureRequest): Promise<ConfigureResponse> {
|
|
281
448
|
const serverRequest = toServerConfigureRequest(request);
|
|
449
|
+
// Configuration changes who the account trusts: always the wallet.
|
|
282
450
|
const response = await this.fetchAuthenticated('/configure', {
|
|
283
451
|
method: 'POST',
|
|
284
452
|
body: JSON.stringify(serverRequest),
|
|
@@ -292,7 +460,7 @@ export class GuardianHttpClient {
|
|
|
292
460
|
const params = new URLSearchParams(requestQuery);
|
|
293
461
|
const response = await this.fetchAuthenticated(`/state?${params}`, {
|
|
294
462
|
method: 'GET',
|
|
295
|
-
}, accountId, requestQuery);
|
|
463
|
+
}, accountId, requestQuery, 'session');
|
|
296
464
|
const server = (await response.json()) as ServerStateObject;
|
|
297
465
|
return fromServerStateObject(server);
|
|
298
466
|
}
|
|
@@ -309,7 +477,7 @@ export class GuardianHttpClient {
|
|
|
309
477
|
const params = new URLSearchParams(requestQuery);
|
|
310
478
|
const response = await this.fetchAuthenticated(`/state/nonce?${params}`, {
|
|
311
479
|
method: 'GET',
|
|
312
|
-
}, accountId, requestQuery);
|
|
480
|
+
}, accountId, requestQuery, 'session');
|
|
313
481
|
const server = (await response.json()) as ServerCanonicalNonceResponse;
|
|
314
482
|
return fromServerCanonicalNonce(server);
|
|
315
483
|
}
|
|
@@ -336,7 +504,7 @@ export class GuardianHttpClient {
|
|
|
336
504
|
const params = new URLSearchParams(requestQuery);
|
|
337
505
|
const response = await this.fetchAuthenticated(`/delta/proposal?${params}`, {
|
|
338
506
|
method: 'GET',
|
|
339
|
-
}, accountId, requestQuery);
|
|
507
|
+
}, accountId, requestQuery, 'session');
|
|
340
508
|
const data = (await response.json()) as ServerProposalsResponse;
|
|
341
509
|
return data.proposals.map(fromServerDeltaObject);
|
|
342
510
|
}
|
|
@@ -346,7 +514,7 @@ export class GuardianHttpClient {
|
|
|
346
514
|
const params = new URLSearchParams(requestQuery);
|
|
347
515
|
const response = await this.fetchAuthenticated(`/delta/proposal/single?${params}`, {
|
|
348
516
|
method: 'GET',
|
|
349
|
-
}, accountId, requestQuery);
|
|
517
|
+
}, accountId, requestQuery, 'session');
|
|
350
518
|
const data = (await response.json()) as ServerDeltaObject;
|
|
351
519
|
return fromServerDeltaObject(data);
|
|
352
520
|
}
|
|
@@ -356,7 +524,7 @@ export class GuardianHttpClient {
|
|
|
356
524
|
const response = await this.fetchAuthenticated('/delta/proposal', {
|
|
357
525
|
method: 'POST',
|
|
358
526
|
body: JSON.stringify(serverRequest),
|
|
359
|
-
}, request.accountId, serverRequest);
|
|
527
|
+
}, request.accountId, serverRequest, 'session');
|
|
360
528
|
const server = (await response.json()) as ServerDeltaProposalResponse;
|
|
361
529
|
return {
|
|
362
530
|
delta: fromServerDeltaObject(server.delta),
|
|
@@ -379,6 +547,7 @@ export class GuardianHttpClient {
|
|
|
379
547
|
*/
|
|
380
548
|
async abandonCandidate(accountId: string, nonce: number): Promise<AbandonCandidateResponse> {
|
|
381
549
|
const serverRequest: ServerAbandonCandidateRequest = { account_id: accountId, nonce };
|
|
550
|
+
// Abandoning changes the account's canonical chain: always the wallet.
|
|
382
551
|
const response = await this.fetchAuthenticated('/delta/candidate/abandon', {
|
|
383
552
|
method: 'POST',
|
|
384
553
|
body: JSON.stringify(serverRequest),
|
|
@@ -438,13 +607,14 @@ export class GuardianHttpClient {
|
|
|
438
607
|
const response = await this.fetchAuthenticated('/delta/proposal', {
|
|
439
608
|
method: 'PUT',
|
|
440
609
|
body: JSON.stringify(serverRequest),
|
|
441
|
-
}, request.accountId, serverRequest);
|
|
610
|
+
}, request.accountId, serverRequest, 'session');
|
|
442
611
|
const server = (await response.json()) as ServerDeltaObject;
|
|
443
612
|
return fromServerDeltaObject(server);
|
|
444
613
|
}
|
|
445
614
|
|
|
446
615
|
async pushDelta(delta: ExecutionDelta): Promise<PushDeltaResponse> {
|
|
447
616
|
const serverDelta = toServerExecutionDelta(delta);
|
|
617
|
+
// Guardian's ACK commits the account to this transition: always the wallet.
|
|
448
618
|
const response = await this.fetchAuthenticated('/delta', {
|
|
449
619
|
method: 'POST',
|
|
450
620
|
body: JSON.stringify(serverDelta),
|
|
@@ -472,7 +642,7 @@ export class GuardianHttpClient {
|
|
|
472
642
|
const params = new URLSearchParams(requestQuery);
|
|
473
643
|
const response = await this.fetchAuthenticated(`/delta?${params}`, {
|
|
474
644
|
method: 'GET',
|
|
475
|
-
}, accountId, requestPayload);
|
|
645
|
+
}, accountId, requestPayload, 'session');
|
|
476
646
|
const server = (await response.json()) as ServerDeltaObject;
|
|
477
647
|
return fromServerDeltaObject(server);
|
|
478
648
|
}
|
|
@@ -489,7 +659,7 @@ export class GuardianHttpClient {
|
|
|
489
659
|
const params = new URLSearchParams(requestQuery);
|
|
490
660
|
const response = await this.fetchAuthenticated(`/delta/since?${params}`, {
|
|
491
661
|
method: 'GET',
|
|
492
|
-
}, accountId, requestPayload);
|
|
662
|
+
}, accountId, requestPayload, 'session');
|
|
493
663
|
const server = (await response.json()) as ServerDeltaObject;
|
|
494
664
|
return fromServerDeltaObject(server);
|
|
495
665
|
}
|
|
@@ -516,7 +686,7 @@ export class GuardianHttpClient {
|
|
|
516
686
|
const params = new URLSearchParams(requestPayload);
|
|
517
687
|
const response = await this.fetchAuthenticated(`/delta/history?${params}`, {
|
|
518
688
|
method: 'GET',
|
|
519
|
-
}, accountId, requestPayload);
|
|
689
|
+
}, accountId, requestPayload, 'session');
|
|
520
690
|
const server = (await response.json()) as ServerHistoryPage;
|
|
521
691
|
return fromServerHistoryPage(server);
|
|
522
692
|
}
|
|
@@ -573,10 +743,7 @@ export class GuardianHttpClient {
|
|
|
573
743
|
...init,
|
|
574
744
|
headers: {
|
|
575
745
|
...init.headers,
|
|
576
|
-
|
|
577
|
-
'x-signature': signature,
|
|
578
|
-
'x-timestamp': timestamp.toString(),
|
|
579
|
-
...(this.signer.requestAuthFormat ? { 'x-auth-format': this.signer.requestAuthFormat } : {}),
|
|
746
|
+
...authHeaders(this.signer.publicKey, signature, timestamp, this.signer.requestAuthFormat),
|
|
580
747
|
},
|
|
581
748
|
});
|
|
582
749
|
}
|
|
@@ -586,30 +753,40 @@ export class GuardianHttpClient {
|
|
|
586
753
|
init: RequestInit,
|
|
587
754
|
accountId: string,
|
|
588
755
|
requestPayload: unknown,
|
|
756
|
+
mode: AuthMode = 'wallet',
|
|
589
757
|
retries = 2
|
|
590
758
|
): Promise<Response> {
|
|
591
|
-
if (!this.signer) {
|
|
592
|
-
throw new Error('No signer configured. Call setSigner() first.');
|
|
593
|
-
}
|
|
594
|
-
|
|
595
759
|
const timestamp = this.nextTimestamp();
|
|
596
760
|
const authPayload = RequestAuthPayload.fromRequest(requestPayload);
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
761
|
+
const session = mode === 'session' ? this.getSession() : null;
|
|
762
|
+
const headers = session
|
|
763
|
+
? authHeaders(
|
|
764
|
+
session.publicKey,
|
|
765
|
+
await session.signRequest(accountId, timestamp, authPayload),
|
|
766
|
+
timestamp,
|
|
767
|
+
'session'
|
|
768
|
+
)
|
|
769
|
+
: await this.walletAuthHeaders(accountId, timestamp, authPayload);
|
|
600
770
|
|
|
601
771
|
try {
|
|
602
772
|
return await this.fetch(path, {
|
|
603
773
|
...init,
|
|
604
774
|
headers: {
|
|
605
775
|
...init.headers,
|
|
606
|
-
|
|
607
|
-
'x-signature': signature,
|
|
608
|
-
'x-timestamp': timestamp.toString(),
|
|
609
|
-
...(this.signer.requestAuthFormat ? { 'x-auth-format': this.signer.requestAuthFormat } : {}),
|
|
776
|
+
...headers,
|
|
610
777
|
},
|
|
611
778
|
});
|
|
612
779
|
} catch (err) {
|
|
780
|
+
// A session Guardian ended or no longer accepts (unknown key after a
|
|
781
|
+
// restart, rotated Guardian key or network) is dead for good: stop
|
|
782
|
+
// using it so the caller can start a new one; meanwhile the wallet
|
|
783
|
+
// signs again.
|
|
784
|
+
if (session && err instanceof GuardianHttpError) {
|
|
785
|
+
const reason = SESSION_END_REASONS[err.code ?? ''];
|
|
786
|
+
if (reason) {
|
|
787
|
+
this.endSession(session, reason);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
613
790
|
// Replay rejections are transient: the request was correctly signed
|
|
614
791
|
// but lost the server's per-signer timestamp CAS. Retry with a fresh
|
|
615
792
|
// timestamp and signature, branching only on the dedicated
|
|
@@ -622,9 +799,23 @@ export class GuardianHttpClient {
|
|
|
622
799
|
err.code === 'authentication_replay'
|
|
623
800
|
) {
|
|
624
801
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
625
|
-
return this.fetchAuthenticated(path, init, accountId, requestPayload, retries - 1);
|
|
802
|
+
return this.fetchAuthenticated(path, init, accountId, requestPayload, mode, retries - 1);
|
|
626
803
|
}
|
|
627
804
|
throw err;
|
|
628
805
|
}
|
|
629
806
|
}
|
|
807
|
+
|
|
808
|
+
private async walletAuthHeaders(
|
|
809
|
+
accountId: string,
|
|
810
|
+
timestamp: number,
|
|
811
|
+
authPayload: RequestAuthPayload
|
|
812
|
+
): Promise<Record<string, string>> {
|
|
813
|
+
if (!this.signer) {
|
|
814
|
+
throw new Error('No signer configured. Call setSigner() first.');
|
|
815
|
+
}
|
|
816
|
+
const signature = this.signer.signRequest
|
|
817
|
+
? await this.signer.signRequest(accountId, timestamp, authPayload)
|
|
818
|
+
: await this.signer.signAccountIdWithTimestamp(accountId, timestamp);
|
|
819
|
+
return authHeaders(this.signer.publicKey, signature, timestamp, this.signer.requestAuthFormat);
|
|
820
|
+
}
|
|
630
821
|
}
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,12 @@ export type {
|
|
|
29
29
|
ConfigureResponse,
|
|
30
30
|
PubkeyResponse,
|
|
31
31
|
StatusResponse,
|
|
32
|
+
SessionsStatus,
|
|
33
|
+
SessionGrantFields,
|
|
34
|
+
SessionRequestSigner,
|
|
35
|
+
SessionEndReason,
|
|
36
|
+
CreateSessionRequest,
|
|
37
|
+
CreateSessionResponse,
|
|
32
38
|
DeltaProposalRequest,
|
|
33
39
|
DeltaProposalResponse,
|
|
34
40
|
ProposalsResponse,
|
package/src/server-types.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type ServerDeltaStatus =
|
|
|
22
22
|
| { status: 'pending'; timestamp: string; proposer_id: string; cosigner_sigs: ServerCosignerSignature[] }
|
|
23
23
|
| { status: 'candidate'; timestamp: string }
|
|
24
24
|
| { status: 'canonical'; timestamp: string }
|
|
25
|
-
| { status: 'retained'; timestamp: string; reason?: 'retry_exhausted' | 'diverged' }
|
|
25
|
+
| { status: 'retained'; timestamp: string; reason?: 'retry_exhausted' | 'diverged' | 'orphaned' }
|
|
26
26
|
| { status: 'discarded'; timestamp: string; reason?: string };
|
|
27
27
|
|
|
28
28
|
export type ServerProposalType =
|
|
@@ -58,7 +58,9 @@ export interface ServerProposalMetadata {
|
|
|
58
58
|
amount?: string;
|
|
59
59
|
/** P2ID note visibility, "public" or "private" (issue #322). Absent => public. */
|
|
60
60
|
note_type?: string;
|
|
61
|
-
/**
|
|
61
|
+
/** The block the proposal's transaction summary binds (unsigned). */
|
|
62
|
+
bound_block_num?: number;
|
|
63
|
+
/** Legacy base64 Miden `ChainAnchor` written by 0.18 clients. */
|
|
62
64
|
chain_anchor?: string;
|
|
63
65
|
/** P2IDE reclaim block height (issue #366). Presence of either height means a P2IDE note. */
|
|
64
66
|
reclaim_height?: number;
|
|
@@ -183,6 +185,42 @@ export interface ServerStatusResponse {
|
|
|
183
185
|
environment: string;
|
|
184
186
|
started_at: string;
|
|
185
187
|
uptime_seconds: number;
|
|
188
|
+
sessions?: {
|
|
189
|
+
max_ttl_seconds: number;
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface ServerCreateSessionRequest {
|
|
194
|
+
scheme: string;
|
|
195
|
+
auth_format?: string;
|
|
196
|
+
public_key?: string;
|
|
197
|
+
signature: string;
|
|
198
|
+
grant: {
|
|
199
|
+
signer_commitment: string;
|
|
200
|
+
session_public_key: string;
|
|
201
|
+
origin: string;
|
|
202
|
+
issued_at: number;
|
|
203
|
+
expires_at: number;
|
|
204
|
+
guardian_commitment: string;
|
|
205
|
+
network: string;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface ServerCreateSessionResponse {
|
|
210
|
+
signer_commitment: string;
|
|
211
|
+
expires_at: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface ServerRevokeSessionResponse {
|
|
215
|
+
revoked: boolean;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface ServerRevokeAllSessionsRequest {
|
|
219
|
+
signer_commitment: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface ServerRevokeAllSessionsResponse {
|
|
223
|
+
revoked: number;
|
|
186
224
|
}
|
|
187
225
|
|
|
188
226
|
export interface ServerLookupAccount {
|
package/src/types.ts
CHANGED
|
@@ -31,6 +31,110 @@ export interface Signer {
|
|
|
31
31
|
keyCommitmentHex: string,
|
|
32
32
|
timestampMs: number
|
|
33
33
|
): Promise<string> | string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sign a session grant once, authorizing a delegated signer to sign
|
|
37
|
+
* per-account requests. Raw signers sign the grant's RPO digest; EIP-712
|
|
38
|
+
* signers sign readable `GuardianSession` typed data. The canonical
|
|
39
|
+
* implementation lives in `@openzeppelin/miden-multisig-client`.
|
|
40
|
+
*/
|
|
41
|
+
signSessionGrant?(grant: SessionGrantFields): Promise<string> | string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sign the account-less revoke-all message for `POST /session/revoke-all`,
|
|
45
|
+
* which ends every session of this signer on the Guardian. Raw signers sign
|
|
46
|
+
* its RPO digest; EIP-712 signers sign `GuardianSessionRevokeAll` typed data.
|
|
47
|
+
*/
|
|
48
|
+
signSessionRevokeAll?(
|
|
49
|
+
signerCommitment: string,
|
|
50
|
+
timestampMs: number
|
|
51
|
+
): Promise<string> | string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fields of a session grant, as the wallet signs and displays them. The scope
|
|
56
|
+
* is fixed in v1 (every account the signer cosigns on this Guardian) and is
|
|
57
|
+
* not a field.
|
|
58
|
+
*/
|
|
59
|
+
export interface SessionGrantFields {
|
|
60
|
+
/** Hex commitment of the wallet key that signs the grant. */
|
|
61
|
+
signerCommitment: string;
|
|
62
|
+
/** Hex SEC1-compressed P-256 delegated-signer public key (33 bytes). */
|
|
63
|
+
sessionPublicKey: string;
|
|
64
|
+
/**
|
|
65
|
+
* The website asking for the session, e.g. `https://app.example`; empty
|
|
66
|
+
* outside a browser. The wallet displays it so the user sees who asks;
|
|
67
|
+
* Guardian does not check it against requests.
|
|
68
|
+
*/
|
|
69
|
+
origin: string;
|
|
70
|
+
/** Unix seconds. */
|
|
71
|
+
issuedAt: number;
|
|
72
|
+
/** Unix seconds. */
|
|
73
|
+
expiresAt: number;
|
|
74
|
+
/** This Guardian's ACK key commitment for the wallet's scheme (`GET /pubkey`). */
|
|
75
|
+
guardianCommitment: string;
|
|
76
|
+
/** `environment` from `GET /status`. */
|
|
77
|
+
network: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Why a client stopped using a session:
|
|
82
|
+
* - `logout`: the app ended it with `revokeSession` or `revokeAllSessions`
|
|
83
|
+
* (the multisig SDK also logs out a session it replaces); do not start a
|
|
84
|
+
* new one in response;
|
|
85
|
+
* - `expired`: it reached its expiry;
|
|
86
|
+
* - `revoked`: Guardian answered `session_revoked` (logout or revoke-all
|
|
87
|
+
* elsewhere);
|
|
88
|
+
* - `rejected`: Guardian no longer accepts it (`authentication_failed`, e.g.
|
|
89
|
+
* after a restart forgot it or the Guardian key was rotated).
|
|
90
|
+
*/
|
|
91
|
+
export type SessionEndReason = 'logout' | 'expired' | 'revoked' | 'rejected';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Signs session-eligible requests on behalf of a wallet after a session grant
|
|
95
|
+
* was registered. Requests carry `x-auth-format: session`.
|
|
96
|
+
*/
|
|
97
|
+
export interface SessionRequestSigner {
|
|
98
|
+
/** Hex SEC1-compressed P-256 session public key, sent as `x-pubkey`. */
|
|
99
|
+
readonly publicKey: string;
|
|
100
|
+
/**
|
|
101
|
+
* Commitment of the wallet that granted the session. The client only uses
|
|
102
|
+
* the session while that wallet is its signer.
|
|
103
|
+
*/
|
|
104
|
+
readonly signerCommitment: string;
|
|
105
|
+
/** Unix seconds. */
|
|
106
|
+
readonly expiresAt: number;
|
|
107
|
+
signRequest(
|
|
108
|
+
accountId: string,
|
|
109
|
+
timestamp: number,
|
|
110
|
+
requestPayload: RequestAuthPayload
|
|
111
|
+
): Promise<string> | string;
|
|
112
|
+
/** Sign the account-less `SessionLogoutMessage` for `POST /session/logout`. */
|
|
113
|
+
signLogout(timestampMs: number): Promise<string> | string;
|
|
114
|
+
/** Called once when the client stops using the session. */
|
|
115
|
+
onEnded?(reason: SessionEndReason): void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface CreateSessionRequest {
|
|
119
|
+
scheme: SignatureScheme;
|
|
120
|
+
/** `eip712` for EIP-712 wallet signatures; omitted for raw signatures. */
|
|
121
|
+
authFormat?: 'eip712';
|
|
122
|
+
/** Wallet public key; required for ECDSA. */
|
|
123
|
+
publicKey?: string;
|
|
124
|
+
signature: string;
|
|
125
|
+
grant: SessionGrantFields;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface CreateSessionResponse {
|
|
129
|
+
signerCommitment: string;
|
|
130
|
+
/** RFC 3339 UTC. */
|
|
131
|
+
expiresAt: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Session parameters a Guardian advertises on `GET /status`. */
|
|
135
|
+
export interface SessionsStatus {
|
|
136
|
+
/** Grants may expire at most this many seconds after registration. */
|
|
137
|
+
maxTtlSeconds: number;
|
|
34
138
|
}
|
|
35
139
|
|
|
36
140
|
export interface FalconSignature {
|
|
@@ -71,11 +175,17 @@ export type DeltaStatus =
|
|
|
71
175
|
| { status: 'pending'; timestamp: string; proposerId: string; cosignerSigs: CosignerSignature[] }
|
|
72
176
|
| { status: 'candidate'; timestamp: string }
|
|
73
177
|
| { status: 'canonical'; timestamp: string }
|
|
74
|
-
/** Candidate the Guardian gave up verifying (retry exhaustion
|
|
75
|
-
* confirmed-diverged observation
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
178
|
+
/** Candidate the Guardian gave up verifying (retry exhaustion, a
|
|
179
|
+
* confirmed-diverged observation, or an orphaned position in the
|
|
180
|
+
* account's candidate queue after its predecessor was parked) but
|
|
181
|
+
* kept for background reconciliation (issue #345 / #17); promoted to
|
|
182
|
+
* `canonical` if the chain ever shows it landed, dropped after a
|
|
183
|
+
* server-side TTL otherwise. */
|
|
184
|
+
| {
|
|
185
|
+
status: 'retained';
|
|
186
|
+
timestamp: string;
|
|
187
|
+
reason?: 'retry_exhausted' | 'diverged' | 'orphaned';
|
|
188
|
+
}
|
|
79
189
|
| { status: 'discarded'; timestamp: string; reason?: string };
|
|
80
190
|
|
|
81
191
|
export type ProposalType =
|
|
@@ -112,11 +222,13 @@ export interface ProposalMetadata {
|
|
|
112
222
|
/** P2ID note visibility, "public" or "private" (issue #322). Absent => public. */
|
|
113
223
|
noteType?: string;
|
|
114
224
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
225
|
+
* The block the proposal's transaction summary binds. Not signed: the
|
|
226
|
+
* multisig SDKs read the block from the summary and refuse a proposal whose
|
|
227
|
+
* value disagrees, so a wrong value can deny a proposal but never get a
|
|
228
|
+
* wrong one accepted.
|
|
119
229
|
*/
|
|
230
|
+
boundBlockNum?: number;
|
|
231
|
+
/** Legacy: the base64 Miden `ChainAnchor` 0.18 clients wrote instead of `boundBlockNum`. */
|
|
120
232
|
chainAnchor?: string;
|
|
121
233
|
/** P2IDE reclaim block height (issue #366). Presence of either height means a P2IDE note. */
|
|
122
234
|
reclaimHeight?: number;
|
|
@@ -198,6 +310,8 @@ export interface StatusResponse {
|
|
|
198
310
|
environment: string;
|
|
199
311
|
startedAt: string;
|
|
200
312
|
uptimeSeconds: number;
|
|
313
|
+
/** Present on every Guardian that accepts session grants; absent on older servers. */
|
|
314
|
+
sessions?: SessionsStatus;
|
|
201
315
|
}
|
|
202
316
|
|
|
203
317
|
export interface DeltaProposalRequest {
|