@openzeppelin/guardian-client 0.18.0-rc.3 → 0.18.1
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 +46 -2
- 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 +41 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +170 -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 +211 -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,116 @@ 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
|
+
|
|
257
412
|
async getPubkey(scheme?: SignatureScheme): Promise<PubkeyResponse> {
|
|
258
413
|
const query = scheme ? `?scheme=${scheme}` : '';
|
|
259
414
|
const response = await this.fetch(`/pubkey${query}`, { method: 'GET' });
|
|
@@ -274,11 +429,19 @@ export class GuardianHttpClient {
|
|
|
274
429
|
environment: data.environment,
|
|
275
430
|
startedAt: data.started_at,
|
|
276
431
|
uptimeSeconds: data.uptime_seconds,
|
|
432
|
+
...(data.sessions
|
|
433
|
+
? {
|
|
434
|
+
sessions: {
|
|
435
|
+
maxTtlSeconds: data.sessions.max_ttl_seconds,
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
: {}),
|
|
277
439
|
};
|
|
278
440
|
}
|
|
279
441
|
|
|
280
442
|
async configure(request: ConfigureRequest): Promise<ConfigureResponse> {
|
|
281
443
|
const serverRequest = toServerConfigureRequest(request);
|
|
444
|
+
// Configuration changes who the account trusts: always the wallet.
|
|
282
445
|
const response = await this.fetchAuthenticated('/configure', {
|
|
283
446
|
method: 'POST',
|
|
284
447
|
body: JSON.stringify(serverRequest),
|
|
@@ -292,7 +455,7 @@ export class GuardianHttpClient {
|
|
|
292
455
|
const params = new URLSearchParams(requestQuery);
|
|
293
456
|
const response = await this.fetchAuthenticated(`/state?${params}`, {
|
|
294
457
|
method: 'GET',
|
|
295
|
-
}, accountId, requestQuery);
|
|
458
|
+
}, accountId, requestQuery, 'session');
|
|
296
459
|
const server = (await response.json()) as ServerStateObject;
|
|
297
460
|
return fromServerStateObject(server);
|
|
298
461
|
}
|
|
@@ -309,7 +472,7 @@ export class GuardianHttpClient {
|
|
|
309
472
|
const params = new URLSearchParams(requestQuery);
|
|
310
473
|
const response = await this.fetchAuthenticated(`/state/nonce?${params}`, {
|
|
311
474
|
method: 'GET',
|
|
312
|
-
}, accountId, requestQuery);
|
|
475
|
+
}, accountId, requestQuery, 'session');
|
|
313
476
|
const server = (await response.json()) as ServerCanonicalNonceResponse;
|
|
314
477
|
return fromServerCanonicalNonce(server);
|
|
315
478
|
}
|
|
@@ -336,7 +499,7 @@ export class GuardianHttpClient {
|
|
|
336
499
|
const params = new URLSearchParams(requestQuery);
|
|
337
500
|
const response = await this.fetchAuthenticated(`/delta/proposal?${params}`, {
|
|
338
501
|
method: 'GET',
|
|
339
|
-
}, accountId, requestQuery);
|
|
502
|
+
}, accountId, requestQuery, 'session');
|
|
340
503
|
const data = (await response.json()) as ServerProposalsResponse;
|
|
341
504
|
return data.proposals.map(fromServerDeltaObject);
|
|
342
505
|
}
|
|
@@ -346,7 +509,7 @@ export class GuardianHttpClient {
|
|
|
346
509
|
const params = new URLSearchParams(requestQuery);
|
|
347
510
|
const response = await this.fetchAuthenticated(`/delta/proposal/single?${params}`, {
|
|
348
511
|
method: 'GET',
|
|
349
|
-
}, accountId, requestQuery);
|
|
512
|
+
}, accountId, requestQuery, 'session');
|
|
350
513
|
const data = (await response.json()) as ServerDeltaObject;
|
|
351
514
|
return fromServerDeltaObject(data);
|
|
352
515
|
}
|
|
@@ -356,7 +519,7 @@ export class GuardianHttpClient {
|
|
|
356
519
|
const response = await this.fetchAuthenticated('/delta/proposal', {
|
|
357
520
|
method: 'POST',
|
|
358
521
|
body: JSON.stringify(serverRequest),
|
|
359
|
-
}, request.accountId, serverRequest);
|
|
522
|
+
}, request.accountId, serverRequest, 'session');
|
|
360
523
|
const server = (await response.json()) as ServerDeltaProposalResponse;
|
|
361
524
|
return {
|
|
362
525
|
delta: fromServerDeltaObject(server.delta),
|
|
@@ -379,6 +542,7 @@ export class GuardianHttpClient {
|
|
|
379
542
|
*/
|
|
380
543
|
async abandonCandidate(accountId: string, nonce: number): Promise<AbandonCandidateResponse> {
|
|
381
544
|
const serverRequest: ServerAbandonCandidateRequest = { account_id: accountId, nonce };
|
|
545
|
+
// Abandoning changes the account's canonical chain: always the wallet.
|
|
382
546
|
const response = await this.fetchAuthenticated('/delta/candidate/abandon', {
|
|
383
547
|
method: 'POST',
|
|
384
548
|
body: JSON.stringify(serverRequest),
|
|
@@ -438,13 +602,14 @@ export class GuardianHttpClient {
|
|
|
438
602
|
const response = await this.fetchAuthenticated('/delta/proposal', {
|
|
439
603
|
method: 'PUT',
|
|
440
604
|
body: JSON.stringify(serverRequest),
|
|
441
|
-
}, request.accountId, serverRequest);
|
|
605
|
+
}, request.accountId, serverRequest, 'session');
|
|
442
606
|
const server = (await response.json()) as ServerDeltaObject;
|
|
443
607
|
return fromServerDeltaObject(server);
|
|
444
608
|
}
|
|
445
609
|
|
|
446
610
|
async pushDelta(delta: ExecutionDelta): Promise<PushDeltaResponse> {
|
|
447
611
|
const serverDelta = toServerExecutionDelta(delta);
|
|
612
|
+
// Guardian's ACK commits the account to this transition: always the wallet.
|
|
448
613
|
const response = await this.fetchAuthenticated('/delta', {
|
|
449
614
|
method: 'POST',
|
|
450
615
|
body: JSON.stringify(serverDelta),
|
|
@@ -472,7 +637,7 @@ export class GuardianHttpClient {
|
|
|
472
637
|
const params = new URLSearchParams(requestQuery);
|
|
473
638
|
const response = await this.fetchAuthenticated(`/delta?${params}`, {
|
|
474
639
|
method: 'GET',
|
|
475
|
-
}, accountId, requestPayload);
|
|
640
|
+
}, accountId, requestPayload, 'session');
|
|
476
641
|
const server = (await response.json()) as ServerDeltaObject;
|
|
477
642
|
return fromServerDeltaObject(server);
|
|
478
643
|
}
|
|
@@ -489,7 +654,7 @@ export class GuardianHttpClient {
|
|
|
489
654
|
const params = new URLSearchParams(requestQuery);
|
|
490
655
|
const response = await this.fetchAuthenticated(`/delta/since?${params}`, {
|
|
491
656
|
method: 'GET',
|
|
492
|
-
}, accountId, requestPayload);
|
|
657
|
+
}, accountId, requestPayload, 'session');
|
|
493
658
|
const server = (await response.json()) as ServerDeltaObject;
|
|
494
659
|
return fromServerDeltaObject(server);
|
|
495
660
|
}
|
|
@@ -516,7 +681,7 @@ export class GuardianHttpClient {
|
|
|
516
681
|
const params = new URLSearchParams(requestPayload);
|
|
517
682
|
const response = await this.fetchAuthenticated(`/delta/history?${params}`, {
|
|
518
683
|
method: 'GET',
|
|
519
|
-
}, accountId, requestPayload);
|
|
684
|
+
}, accountId, requestPayload, 'session');
|
|
520
685
|
const server = (await response.json()) as ServerHistoryPage;
|
|
521
686
|
return fromServerHistoryPage(server);
|
|
522
687
|
}
|
|
@@ -573,10 +738,7 @@ export class GuardianHttpClient {
|
|
|
573
738
|
...init,
|
|
574
739
|
headers: {
|
|
575
740
|
...init.headers,
|
|
576
|
-
|
|
577
|
-
'x-signature': signature,
|
|
578
|
-
'x-timestamp': timestamp.toString(),
|
|
579
|
-
...(this.signer.requestAuthFormat ? { 'x-auth-format': this.signer.requestAuthFormat } : {}),
|
|
741
|
+
...authHeaders(this.signer.publicKey, signature, timestamp, this.signer.requestAuthFormat),
|
|
580
742
|
},
|
|
581
743
|
});
|
|
582
744
|
}
|
|
@@ -586,30 +748,40 @@ export class GuardianHttpClient {
|
|
|
586
748
|
init: RequestInit,
|
|
587
749
|
accountId: string,
|
|
588
750
|
requestPayload: unknown,
|
|
751
|
+
mode: AuthMode = 'wallet',
|
|
589
752
|
retries = 2
|
|
590
753
|
): Promise<Response> {
|
|
591
|
-
if (!this.signer) {
|
|
592
|
-
throw new Error('No signer configured. Call setSigner() first.');
|
|
593
|
-
}
|
|
594
|
-
|
|
595
754
|
const timestamp = this.nextTimestamp();
|
|
596
755
|
const authPayload = RequestAuthPayload.fromRequest(requestPayload);
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
756
|
+
const session = mode === 'session' ? this.getSession() : null;
|
|
757
|
+
const headers = session
|
|
758
|
+
? authHeaders(
|
|
759
|
+
session.publicKey,
|
|
760
|
+
await session.signRequest(accountId, timestamp, authPayload),
|
|
761
|
+
timestamp,
|
|
762
|
+
'session'
|
|
763
|
+
)
|
|
764
|
+
: await this.walletAuthHeaders(accountId, timestamp, authPayload);
|
|
600
765
|
|
|
601
766
|
try {
|
|
602
767
|
return await this.fetch(path, {
|
|
603
768
|
...init,
|
|
604
769
|
headers: {
|
|
605
770
|
...init.headers,
|
|
606
|
-
|
|
607
|
-
'x-signature': signature,
|
|
608
|
-
'x-timestamp': timestamp.toString(),
|
|
609
|
-
...(this.signer.requestAuthFormat ? { 'x-auth-format': this.signer.requestAuthFormat } : {}),
|
|
771
|
+
...headers,
|
|
610
772
|
},
|
|
611
773
|
});
|
|
612
774
|
} catch (err) {
|
|
775
|
+
// A session Guardian ended or no longer accepts (unknown key after a
|
|
776
|
+
// restart, rotated Guardian key or network) is dead for good: stop
|
|
777
|
+
// using it so the caller can start a new one; meanwhile the wallet
|
|
778
|
+
// signs again.
|
|
779
|
+
if (session && err instanceof GuardianHttpError) {
|
|
780
|
+
const reason = SESSION_END_REASONS[err.code ?? ''];
|
|
781
|
+
if (reason) {
|
|
782
|
+
this.endSession(session, reason);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
613
785
|
// Replay rejections are transient: the request was correctly signed
|
|
614
786
|
// but lost the server's per-signer timestamp CAS. Retry with a fresh
|
|
615
787
|
// timestamp and signature, branching only on the dedicated
|
|
@@ -622,9 +794,23 @@ export class GuardianHttpClient {
|
|
|
622
794
|
err.code === 'authentication_replay'
|
|
623
795
|
) {
|
|
624
796
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
625
|
-
return this.fetchAuthenticated(path, init, accountId, requestPayload, retries - 1);
|
|
797
|
+
return this.fetchAuthenticated(path, init, accountId, requestPayload, mode, retries - 1);
|
|
626
798
|
}
|
|
627
799
|
throw err;
|
|
628
800
|
}
|
|
629
801
|
}
|
|
802
|
+
|
|
803
|
+
private async walletAuthHeaders(
|
|
804
|
+
accountId: string,
|
|
805
|
+
timestamp: number,
|
|
806
|
+
authPayload: RequestAuthPayload
|
|
807
|
+
): Promise<Record<string, string>> {
|
|
808
|
+
if (!this.signer) {
|
|
809
|
+
throw new Error('No signer configured. Call setSigner() first.');
|
|
810
|
+
}
|
|
811
|
+
const signature = this.signer.signRequest
|
|
812
|
+
? await this.signer.signRequest(accountId, timestamp, authPayload)
|
|
813
|
+
: await this.signer.signAccountIdWithTimestamp(accountId, timestamp);
|
|
814
|
+
return authHeaders(this.signer.publicKey, signature, timestamp, this.signer.requestAuthFormat);
|
|
815
|
+
}
|
|
630
816
|
}
|
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 {
|