@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/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
- 'x-pubkey': this.signer.publicKey,
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 signature = this.signer.signRequest
598
- ? await this.signer.signRequest(accountId, timestamp, authPayload)
599
- : await this.signer.signAccountIdWithTimestamp(accountId, timestamp);
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
- 'x-pubkey': this.signer.publicKey,
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,
@@ -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
- /** Base64-serialized Miden `ChainAnchor` pinning the proposal's reference block. */
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 or a
75
- * confirmed-diverged observation) but kept for background
76
- * reconciliation (issue #345); promoted to `canonical` if the chain
77
- * ever shows it landed, dropped after a server-side TTL otherwise. */
78
- | { status: 'retained'; timestamp: string; reason?: 'retry_exhausted' | 'diverged' }
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
- * Base64-serialized Miden `ChainAnchor` pinning the reference block the
116
- * proposal's transaction summary was built at. Since protocol 0.16 the
117
- * signed summary binds the reference block commitment, so cosigners and the
118
- * executor need this anchor to reproduce the summary the proposer signed.
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 {