@openzeppelin/miden-multisig-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.
Files changed (104) hide show
  1. package/README.md +117 -25
  2. package/dist/client.d.ts +39 -0
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +77 -0
  5. package/dist/client.js.map +1 -1
  6. package/dist/index.d.ts +5 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +5 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/lookupAuth.d.ts.map +1 -1
  11. package/dist/lookupAuth.js +2 -20
  12. package/dist/lookupAuth.js.map +1 -1
  13. package/dist/multisig/authArgErrors.d.ts +27 -1
  14. package/dist/multisig/authArgErrors.d.ts.map +1 -1
  15. package/dist/multisig/authArgErrors.js +33 -0
  16. package/dist/multisig/authArgErrors.js.map +1 -1
  17. package/dist/multisig.d.ts +77 -11
  18. package/dist/multisig.d.ts.map +1 -1
  19. package/dist/multisig.js +247 -181
  20. package/dist/multisig.js.map +1 -1
  21. package/dist/proposal/metadata.d.ts.map +1 -1
  22. package/dist/proposal/metadata.js +2 -0
  23. package/dist/proposal/metadata.js.map +1 -1
  24. package/dist/recovery/transportDrain.d.ts +38 -30
  25. package/dist/recovery/transportDrain.d.ts.map +1 -1
  26. package/dist/recovery/transportDrain.js +113 -113
  27. package/dist/recovery/transportDrain.js.map +1 -1
  28. package/dist/session/grant.d.ts +40 -0
  29. package/dist/session/grant.d.ts.map +1 -0
  30. package/dist/session/grant.js +125 -0
  31. package/dist/session/grant.js.map +1 -0
  32. package/dist/session/key.d.ts +103 -0
  33. package/dist/session/key.d.ts.map +1 -0
  34. package/dist/session/key.js +160 -0
  35. package/dist/session/key.js.map +1 -0
  36. package/dist/session/manager.d.ts +76 -0
  37. package/dist/session/manager.d.ts.map +1 -0
  38. package/dist/session/manager.js +187 -0
  39. package/dist/session/manager.js.map +1 -0
  40. package/dist/signers/ecdsa.d.ts +5 -1
  41. package/dist/signers/ecdsa.d.ts.map +1 -1
  42. package/dist/signers/ecdsa.js +9 -0
  43. package/dist/signers/ecdsa.js.map +1 -1
  44. package/dist/signers/falcon.d.ts +5 -1
  45. package/dist/signers/falcon.d.ts.map +1 -1
  46. package/dist/signers/falcon.js +9 -0
  47. package/dist/signers/falcon.js.map +1 -1
  48. package/dist/signers/ledger.d.ts +9 -1
  49. package/dist/signers/ledger.d.ts.map +1 -1
  50. package/dist/signers/ledger.js +13 -1
  51. package/dist/signers/ledger.js.map +1 -1
  52. package/dist/signers/miden-wallet.d.ts +13 -1
  53. package/dist/signers/miden-wallet.d.ts.map +1 -1
  54. package/dist/signers/miden-wallet.js +25 -0
  55. package/dist/signers/miden-wallet.js.map +1 -1
  56. package/dist/signers/para.d.ts +5 -1
  57. package/dist/signers/para.d.ts.map +1 -1
  58. package/dist/signers/para.js +9 -0
  59. package/dist/signers/para.js.map +1 -1
  60. package/dist/transaction/authArgs.d.ts +2 -2
  61. package/dist/transaction/authArgs.js +2 -2
  62. package/dist/transaction/options.d.ts +2 -2
  63. package/dist/transaction/summary.d.ts +17 -61
  64. package/dist/transaction/summary.d.ts.map +1 -1
  65. package/dist/transaction/summary.js +49 -95
  66. package/dist/transaction/summary.js.map +1 -1
  67. package/dist/transaction.d.ts +1 -1
  68. package/dist/transaction.d.ts.map +1 -1
  69. package/dist/transaction.js +1 -1
  70. package/dist/transaction.js.map +1 -1
  71. package/dist/types/proposal.d.ts +11 -5
  72. package/dist/types/proposal.d.ts.map +1 -1
  73. package/dist/types/proposal.js.map +1 -1
  74. package/dist/utils/eip712.d.ts +26 -69
  75. package/dist/utils/eip712.d.ts.map +1 -1
  76. package/dist/utils/eip712.js +101 -8
  77. package/dist/utils/eip712.js.map +1 -1
  78. package/dist/utils/felt.d.ts +7 -1
  79. package/dist/utils/felt.d.ts.map +1 -1
  80. package/dist/utils/felt.js +18 -1
  81. package/dist/utils/felt.js.map +1 -1
  82. package/package.json +4 -4
  83. package/src/client.ts +95 -0
  84. package/src/index.ts +27 -6
  85. package/src/lookupAuth.ts +3 -22
  86. package/src/multisig/authArgErrors.ts +42 -1
  87. package/src/multisig.ts +318 -207
  88. package/src/proposal/metadata.ts +2 -0
  89. package/src/recovery/transportDrain.ts +118 -124
  90. package/src/session/grant.ts +143 -0
  91. package/src/session/key.ts +215 -0
  92. package/src/session/manager.ts +273 -0
  93. package/src/signers/ecdsa.ts +12 -1
  94. package/src/signers/falcon.ts +12 -1
  95. package/src/signers/ledger.ts +19 -2
  96. package/src/signers/miden-wallet.ts +29 -1
  97. package/src/signers/para.ts +12 -1
  98. package/src/transaction/authArgs.ts +2 -2
  99. package/src/transaction/options.ts +2 -2
  100. package/src/transaction/summary.ts +62 -110
  101. package/src/transaction.ts +1 -6
  102. package/src/types/proposal.ts +11 -5
  103. package/src/utils/eip712.ts +130 -10
  104. package/src/utils/felt.ts +19 -1
@@ -0,0 +1,215 @@
1
+ import type {
2
+ RequestAuthPayload,
3
+ SessionEndReason,
4
+ SessionRequestSigner,
5
+ } from '@openzeppelin/guardian-client';
6
+ import type { Word } from '@miden-sdk/miden-sdk';
7
+ import { AuthDigest } from '../utils/digest.js';
8
+ import { EcdsaFormat } from '../utils/ecdsa.js';
9
+ import { bytesToHex } from '../utils/encoding.js';
10
+ import { wordToBytes } from '../utils/word.js';
11
+ import { sessionLogoutDigest } from './grant.js';
12
+
13
+ const ALGORITHM = { name: 'ECDSA', namedCurve: 'P-256' } as const;
14
+ const SIGNATURE = { name: 'ECDSA', hash: 'SHA-256' } as const;
15
+
16
+ function subtle(): SubtleCrypto {
17
+ if (!globalThis.crypto?.subtle) {
18
+ throw new Error('Guardian sessions require WebCrypto (crypto.subtle)');
19
+ }
20
+ return globalThis.crypto.subtle;
21
+ }
22
+
23
+ /**
24
+ * A P-256 session key whose private half is a non-extractable WebCrypto key:
25
+ * page scripts can use it to sign while it is loaded, but can never read or
26
+ * export it.
27
+ */
28
+ export class WebCryptoSessionKey {
29
+ constructor(
30
+ readonly privateKey: CryptoKey,
31
+ /** Hex SEC1-compressed public key. */
32
+ readonly publicKey: string,
33
+ ) {}
34
+
35
+ static async generate(): Promise<WebCryptoSessionKey> {
36
+ const pair = await subtle().generateKey(ALGORITHM, false, ['sign', 'verify']);
37
+ const raw = new Uint8Array(await subtle().exportKey('raw', pair.publicKey));
38
+ return new WebCryptoSessionKey(pair.privateKey, EcdsaFormat.compressPublicKey(bytesToHex(raw)));
39
+ }
40
+
41
+ /** Sign the 32 bytes of a word: ECDSA P-256 over SHA-256, `r || s` hex. */
42
+ async signWord(word: Word): Promise<string> {
43
+ const message = new Uint8Array(wordToBytes(word));
44
+ const signature = await subtle().sign(SIGNATURE, this.privateKey, message);
45
+ return bytesToHex(new Uint8Array(signature));
46
+ }
47
+ }
48
+
49
+ export interface GuardianSessionOptions {
50
+ /** The store keeping this session's key; its record is removed when the session ends. */
51
+ store?: SessionKeyStore;
52
+ /**
53
+ * Called once when the Guardian client stops using the session. Requests
54
+ * are then signed by the wallet again. On `expired`, `revoked` or
55
+ * `rejected`, start a new session to stop the prompts; `logout` means the
56
+ * app ended it itself (logout, revoke-all or a replacement session).
57
+ */
58
+ onEnded?: (reason: SessionEndReason, session: GuardianSession) => void;
59
+ }
60
+
61
+ /**
62
+ * Signs Guardian requests for a wallet after its session grant was
63
+ * registered. Plug into `GuardianHttpClient.setSession`.
64
+ */
65
+ export class GuardianSession implements SessionRequestSigner {
66
+ constructor(
67
+ private readonly key: WebCryptoSessionKey,
68
+ /** Commitment of the wallet that signed the grant. */
69
+ readonly signerCommitment: string,
70
+ /** Unix seconds. */
71
+ readonly expiresAt: number,
72
+ /** Where a `SessionKeyStore` keeps this session's key. */
73
+ readonly storeId: string,
74
+ private readonly options: GuardianSessionOptions = {},
75
+ ) {}
76
+
77
+ get publicKey(): string {
78
+ return this.key.publicKey;
79
+ }
80
+
81
+ /**
82
+ * Forgets the stored key (best effort: a dead key left behind fails its
83
+ * first request after a reload and ends then) and tells the app.
84
+ */
85
+ onEnded(reason: SessionEndReason): void {
86
+ if (this.options.store) {
87
+ void forgetSessionKey(this.options.store, this).catch(() => undefined);
88
+ }
89
+ this.options.onEnded?.(reason, this);
90
+ }
91
+
92
+ signRequest(
93
+ accountId: string,
94
+ timestamp: number,
95
+ requestPayload: RequestAuthPayload,
96
+ ): Promise<string> {
97
+ return this.key.signWord(AuthDigest.fromRequest(accountId, timestamp, requestPayload));
98
+ }
99
+
100
+ signLogout(timestampMs: number): Promise<string> {
101
+ return this.key.signWord(sessionLogoutDigest(this.key.publicKey, timestampMs));
102
+ }
103
+ }
104
+
105
+ /** A session key persisted across page loads. */
106
+ export interface StoredSessionKey {
107
+ /** `<guardian key commitment>:<signer commitment>`. */
108
+ id: string;
109
+ privateKey: CryptoKey;
110
+ publicKey: string;
111
+ /** Unix seconds. */
112
+ expiresAt: number;
113
+ }
114
+
115
+ export interface SessionKeyStore {
116
+ load(id: string): Promise<StoredSessionKey | null>;
117
+ save(record: StoredSessionKey): Promise<void>;
118
+ /**
119
+ * Remove the record `id` only if it still holds `publicKey`, as one atomic
120
+ * step, so a newer key another tab saved for the same wallet and Guardian is
121
+ * never deleted.
122
+ */
123
+ remove(id: string, publicKey: string): Promise<void>;
124
+ }
125
+
126
+ /**
127
+ * Remove the stored key of `session`, unless the record already holds the key
128
+ * of a newer session for the same wallet and Guardian.
129
+ */
130
+ export async function forgetSessionKey(
131
+ store: SessionKeyStore,
132
+ session: { storeId: string; publicKey: string },
133
+ ): Promise<void> {
134
+ await store.remove(session.storeId, session.publicKey);
135
+ }
136
+
137
+ /**
138
+ * Keeps session keys in IndexedDB so a reload does not need another wallet
139
+ * signature. The browser stores the `CryptoKey` object itself; it stays
140
+ * non-extractable.
141
+ */
142
+ export class IndexedDbSessionKeyStore implements SessionKeyStore {
143
+ private static readonly STORE = 'sessions';
144
+ private database: Promise<IDBDatabase> | null = null;
145
+
146
+ constructor(private readonly databaseName = 'guardian-sessions') {}
147
+
148
+ async load(id: string): Promise<StoredSessionKey | null> {
149
+ const record = await this.request<StoredSessionKey | undefined>('readonly', (store) =>
150
+ store.get(id),
151
+ );
152
+ return record ?? null;
153
+ }
154
+
155
+ async save(record: StoredSessionKey): Promise<void> {
156
+ await this.request('readwrite', (store) => store.put(record));
157
+ }
158
+
159
+ async remove(id: string, publicKey: string): Promise<void> {
160
+ const database = await this.open();
161
+ return new Promise<void>((resolve, reject) => {
162
+ const transaction = database.transaction(IndexedDbSessionKeyStore.STORE, 'readwrite');
163
+ const store = transaction.objectStore(IndexedDbSessionKeyStore.STORE);
164
+ const lookup = store.get(id);
165
+ lookup.onsuccess = () => {
166
+ if ((lookup.result as StoredSessionKey | undefined)?.publicKey === publicKey) {
167
+ store.delete(id);
168
+ }
169
+ };
170
+ transaction.oncomplete = () => resolve();
171
+ transaction.onerror = () => reject(transaction.error);
172
+ transaction.onabort = () => reject(transaction.error);
173
+ });
174
+ }
175
+
176
+
177
+ private async request<T>(
178
+ mode: IDBTransactionMode,
179
+ run: (store: IDBObjectStore) => IDBRequest,
180
+ ): Promise<T> {
181
+ const database = await this.open();
182
+ return new Promise<T>((resolve, reject) => {
183
+ const transaction = database.transaction(IndexedDbSessionKeyStore.STORE, mode);
184
+ const request = run(transaction.objectStore(IndexedDbSessionKeyStore.STORE));
185
+ transaction.oncomplete = () => resolve(request.result as T);
186
+ transaction.onerror = () => reject(transaction.error ?? request.error);
187
+ transaction.onabort = () => reject(transaction.error ?? request.error);
188
+ });
189
+ }
190
+
191
+ /** Opens the database once; a failed open is retried on the next call. */
192
+ private open(): Promise<IDBDatabase> {
193
+ if (!this.database) {
194
+ const opening = new Promise<IDBDatabase>((resolve, reject) => {
195
+ if (!globalThis.indexedDB) {
196
+ reject(new Error('IndexedDB is not available'));
197
+ return;
198
+ }
199
+ const request = globalThis.indexedDB.open(this.databaseName, 1);
200
+ request.onupgradeneeded = () => {
201
+ request.result.createObjectStore(IndexedDbSessionKeyStore.STORE, { keyPath: 'id' });
202
+ };
203
+ request.onsuccess = () => resolve(request.result);
204
+ request.onerror = () => reject(request.error);
205
+ });
206
+ opening.catch(() => {
207
+ if (this.database === opening) {
208
+ this.database = null;
209
+ }
210
+ });
211
+ this.database = opening;
212
+ }
213
+ return this.database;
214
+ }
215
+ }
@@ -0,0 +1,273 @@
1
+ import type {
2
+ GuardianHttpClient,
3
+ SessionGrantFields,
4
+ Signer,
5
+ } from '@openzeppelin/guardian-client';
6
+ import {
7
+ GuardianSession,
8
+ type GuardianSessionOptions,
9
+ type SessionKeyStore,
10
+ type StoredSessionKey,
11
+ WebCryptoSessionKey,
12
+ forgetSessionKey,
13
+ } from './key.js';
14
+
15
+ /**
16
+ * A grant is shortened by this much below the Guardian's maximum lifetime so
17
+ * client clock drift cannot push it over the limit.
18
+ */
19
+ const MAX_TTL_MARGIN_SECONDS = 60;
20
+
21
+ /**
22
+ * Guardian accepts a grant only while it is at most this old, and only if it
23
+ * expires more than this far in the future.
24
+ */
25
+ const GRANT_WINDOW_SECONDS = 300;
26
+
27
+ /** Matches `GuardianHttpClient`: a session this close to expiry is not used. */
28
+ const SESSION_EXPIRY_MARGIN_SECONDS = 30;
29
+
30
+ /** The Guardian does not advertise sessions on `GET /status`. */
31
+ export class GuardianSessionsUnsupportedError extends Error {
32
+ constructor() {
33
+ super('This Guardian does not accept sessions');
34
+ this.name = 'GuardianSessionsUnsupportedError';
35
+ }
36
+ }
37
+
38
+ /** `confirm` returned `false`: the user declined the grant. */
39
+ export class SessionGrantDeclinedError extends Error {
40
+ constructor() {
41
+ super('Session grant was declined');
42
+ this.name = 'SessionGrantDeclinedError';
43
+ }
44
+ }
45
+
46
+ export interface StartSessionOptions extends Pick<GuardianSessionOptions, 'onEnded'> {
47
+ /**
48
+ * Requested lifetime, more than 300 seconds; capped below the Guardian's
49
+ * advertised maximum.
50
+ */
51
+ ttlSeconds?: number;
52
+ /**
53
+ * The website asking for the session, shown to the user by the wallet.
54
+ * Defaults to the page's `location.origin` in a browser and to none
55
+ * elsewhere. Guardian does not check it against requests.
56
+ */
57
+ origin?: string;
58
+ /**
59
+ * Called with the grant before the wallet signs it; return `false` to
60
+ * abort. Required for raw (non-EIP-712) wallets, which show only a hash:
61
+ * display {@link describeSessionGrant} to the user here.
62
+ */
63
+ confirm?: (grant: SessionGrantFields) => boolean | Promise<boolean>;
64
+ /** Persist the session key so a reload can `resume` without the wallet. */
65
+ store?: SessionKeyStore;
66
+ }
67
+
68
+ /** The current page's origin in a browser; empty elsewhere. */
69
+ function pageOrigin(): string {
70
+ const location = (globalThis as { location?: { origin?: string } }).location;
71
+ return location?.origin && location.origin !== 'null' ? location.origin : '';
72
+ }
73
+
74
+ function storeId(guardianCommitment: string, signerCommitment: string): string {
75
+ return `${guardianCommitment.toLowerCase()}:${signerCommitment.toLowerCase()}`;
76
+ }
77
+
78
+ function nowSeconds(): number {
79
+ return Math.floor(Date.now() / 1000);
80
+ }
81
+
82
+ /**
83
+ * Start a Guardian session: `signer` signs one grant and, once the session is
84
+ * registered, becomes `guardian`'s signer; a non-extractable P-256 delegated
85
+ * signer then signs the
86
+ * session-eligible requests on `guardian` (reads, proposal list/get, proposal
87
+ * create/sign). Every other route, including `configure`, `pushDelta`,
88
+ * `abandonCandidate`, account lookup and `revokeAllSessions`, keeps using the
89
+ * wallet. A session already set on `guardian` is replaced but stays valid on
90
+ * the server until it expires; end it first with {@link endGuardianSession}.
91
+ *
92
+ * The grant is dated after `confirm` returns, so a slow confirmation still
93
+ * registers; only its start moves, the confirmed expiry stays. If storing the
94
+ * key fails, the registered session is logged out and the error rethrown.
95
+ */
96
+ export async function startGuardianSession(
97
+ guardian: GuardianHttpClient,
98
+ signer: Signer,
99
+ options: StartSessionOptions = {},
100
+ ): Promise<GuardianSession> {
101
+ if (!signer.signSessionGrant) {
102
+ throw new Error('This signer cannot sign Guardian session grants');
103
+ }
104
+ if (signer.requestAuthFormat !== 'eip712' && !options.confirm) {
105
+ throw new Error(
106
+ 'This wallet signs a session grant as a hash: pass `confirm` and show ' +
107
+ 'describeSessionGrant(grant) to the user first',
108
+ );
109
+ }
110
+ const [status, { commitment: guardianCommitment }, key] = await Promise.all([
111
+ guardian.getStatus(),
112
+ guardian.getPubkey(signer.scheme),
113
+ WebCryptoSessionKey.generate(),
114
+ ]);
115
+ if (!status.sessions) {
116
+ throw new GuardianSessionsUnsupportedError();
117
+ }
118
+ const maxTtl = status.sessions.maxTtlSeconds - MAX_TTL_MARGIN_SECONDS;
119
+ const ttl = Math.min(options.ttlSeconds ?? maxTtl, maxTtl);
120
+ if (!Number.isInteger(ttl) || ttl <= GRANT_WINDOW_SECONDS) {
121
+ throw new Error(
122
+ `Session lifetime must be a whole number of seconds above ${GRANT_WINDOW_SECONDS}`,
123
+ );
124
+ }
125
+
126
+ const issuedAt = nowSeconds();
127
+ let grant: SessionGrantFields = {
128
+ signerCommitment: signer.commitment,
129
+ sessionPublicKey: key.publicKey,
130
+ origin: options.origin ?? pageOrigin(),
131
+ issuedAt,
132
+ expiresAt: issuedAt + ttl,
133
+ guardianCommitment,
134
+ network: status.environment,
135
+ };
136
+ if (options.confirm) {
137
+ if (!(await options.confirm(grant))) {
138
+ throw new SessionGrantDeclinedError();
139
+ }
140
+ grant = { ...grant, issuedAt: Math.max(issuedAt, nowSeconds()) };
141
+ if (grant.expiresAt - grant.issuedAt <= GRANT_WINDOW_SECONDS) {
142
+ throw new Error('The session grant was confirmed too late; start the session again');
143
+ }
144
+ }
145
+ const signature = await signer.signSessionGrant(grant);
146
+ if (nowSeconds() - grant.issuedAt > GRANT_WINDOW_SECONDS) {
147
+ throw new Error(
148
+ `The wallet took more than ${GRANT_WINDOW_SECONDS} seconds to sign the session grant; ` +
149
+ 'start the session again',
150
+ );
151
+ }
152
+ await guardian.createSession({
153
+ scheme: signer.scheme,
154
+ ...(signer.requestAuthFormat === 'eip712' ? { authFormat: 'eip712' as const } : {}),
155
+ ...(signer.scheme === 'ecdsa' ? { publicKey: signer.publicKey } : {}),
156
+ signature,
157
+ grant,
158
+ });
159
+
160
+ const session = new GuardianSession(
161
+ key,
162
+ signer.commitment,
163
+ grant.expiresAt,
164
+ storeId(guardianCommitment, signer.commitment),
165
+ { store: options.store, onEnded: options.onEnded },
166
+ );
167
+ if (options.store) {
168
+ try {
169
+ await options.store.save({
170
+ id: session.storeId,
171
+ privateKey: key.privateKey,
172
+ publicKey: key.publicKey,
173
+ expiresAt: grant.expiresAt,
174
+ });
175
+ } catch (error) {
176
+ const unreachable = new GuardianSession(key, signer.commitment, grant.expiresAt, session.storeId);
177
+ await guardian.revokeSession(unreachable).catch(() => false);
178
+ throw error;
179
+ }
180
+ }
181
+ guardian.setSigner(signer);
182
+ guardian.setSession(session);
183
+ return session;
184
+ }
185
+
186
+ /**
187
+ * Reuse a session key persisted by `startGuardianSession` without asking the
188
+ * wallet again; `signer` becomes `guardian`'s signer when a session resumes.
189
+ * Returns `null` when none
190
+ * is stored or it expires within 30 seconds. A session revoked elsewhere
191
+ * fails its next request with `session_revoked` and ends then (`onEnded`);
192
+ * start a new one.
193
+ */
194
+ export async function resumeGuardianSession(
195
+ guardian: GuardianHttpClient,
196
+ signer: Signer,
197
+ store: SessionKeyStore,
198
+ options: Pick<GuardianSessionOptions, 'onEnded'> = {},
199
+ ): Promise<GuardianSession | null> {
200
+ const { commitment: guardianCommitment } = await guardian.getPubkey(signer.scheme);
201
+ const id = storeId(guardianCommitment, signer.commitment);
202
+ const record = await store.load(id);
203
+ if (!record) {
204
+ return null;
205
+ }
206
+ if (record.expiresAt - SESSION_EXPIRY_MARGIN_SECONDS <= nowSeconds()) {
207
+ await store.remove(id, record.publicKey);
208
+ return null;
209
+ }
210
+ const session = new GuardianSession(
211
+ new WebCryptoSessionKey(record.privateKey, record.publicKey),
212
+ signer.commitment,
213
+ record.expiresAt,
214
+ id,
215
+ { store, onEnded: options.onEnded },
216
+ );
217
+ guardian.setSigner(signer);
218
+ guardian.setSession(session);
219
+ return session;
220
+ }
221
+
222
+ /**
223
+ * Revoke `session` on the server, stop using it if it is the current one, and
224
+ * forget its stored key. Returns whether the server still had it active. On
225
+ * error nothing changes.
226
+ */
227
+ export async function endGuardianSession(
228
+ guardian: GuardianHttpClient,
229
+ session: GuardianSession,
230
+ store?: SessionKeyStore,
231
+ ): Promise<boolean> {
232
+ const revoked = await guardian.revokeSession(session);
233
+ if (store) {
234
+ await forgetSessionKey(store, session);
235
+ }
236
+ return revoked;
237
+ }
238
+
239
+ /**
240
+ * Revoke every session of `signer` on the Guardian with a wallet signature,
241
+ * including sessions started in other tabs or devices, stop using the current
242
+ * one, and forget its stored key (a newer key another tab stored meanwhile
243
+ * is kept). `signer` becomes `guardian`'s signer. Use it when a session key
244
+ * may be compromised: page logout alone cannot stop a script that holds the
245
+ * key. Returns how many sessions the server revoked.
246
+ *
247
+ * It ends sessions already registered with Guardian. A page that got a grant
248
+ * signed can still register it for up to about 10 minutes, and a device whose
249
+ * clock runs behind can miss sessions started just before; run it again 10
250
+ * minutes later when a key may be compromised.
251
+ */
252
+ export async function revokeAllGuardianSessions(
253
+ guardian: GuardianHttpClient,
254
+ signer: Signer,
255
+ store?: SessionKeyStore,
256
+ ): Promise<number> {
257
+ guardian.setSigner(signer);
258
+ const stored = store ? await storedKey(guardian, signer, store) : null;
259
+ const revoked = await guardian.revokeAllSessions();
260
+ if (store && stored) {
261
+ await forgetSessionKey(store, { storeId: stored.id, publicKey: stored.publicKey });
262
+ }
263
+ return revoked;
264
+ }
265
+
266
+ async function storedKey(
267
+ guardian: GuardianHttpClient,
268
+ signer: Signer,
269
+ store: SessionKeyStore,
270
+ ): Promise<StoredSessionKey | null> {
271
+ const { commitment: guardianCommitment } = await guardian.getPubkey(signer.scheme);
272
+ return store.load(storeId(guardianCommitment, signer.commitment));
273
+ }
@@ -1,9 +1,10 @@
1
- import type { RequestAuthPayload } from '@openzeppelin/guardian-client';
1
+ import type { RequestAuthPayload, SessionGrantFields } from '@openzeppelin/guardian-client';
2
2
  import { AccountId, AuthSecretKey, type MidenClient, type Word } from '@miden-sdk/miden-sdk';
3
3
  import type { Signer, SignatureScheme } from '../types.js';
4
4
  import { bytesToHex, normalizeHexWord } from '../utils/encoding.js';
5
5
  import { AuthDigest } from '../utils/digest.js';
6
6
  import { lookupAuthDigest } from '../lookupAuth.js';
7
+ import { sessionGrantDigest, sessionRevokeAllDigest } from '../session/grant.js';
7
8
 
8
9
  export class EcdsaSigner implements Signer {
9
10
  readonly commitment: string;
@@ -49,6 +50,16 @@ export class EcdsaSigner implements Signer {
49
50
  return this.signWord(digest);
50
51
  }
51
52
 
53
+ /** Sign a session grant as its raw RPO digest. */
54
+ async signSessionGrant(grant: SessionGrantFields): Promise<string> {
55
+ return this.signWord(sessionGrantDigest(grant));
56
+ }
57
+
58
+ /** Sign a session revoke-all as its raw RPO digest. */
59
+ async signSessionRevokeAll(signerCommitment: string, timestampMs: number): Promise<string> {
60
+ return this.signWord(sessionRevokeAllDigest(signerCommitment, timestampMs));
61
+ }
62
+
52
63
  async bindAccountKey(midenClient: MidenClient, accountId: string): Promise<void> {
53
64
  const targetAccountId = AccountId.fromHex(accountId);
54
65
  const existingAccountId = await midenClient.keystore.getAccountId(this.publicKeyCommitment);
@@ -1,9 +1,10 @@
1
- import type { RequestAuthPayload } from '@openzeppelin/guardian-client';
1
+ import type { RequestAuthPayload, SessionGrantFields } from '@openzeppelin/guardian-client';
2
2
  import { AccountId, AuthSecretKey, type MidenClient, type Word } from '@miden-sdk/miden-sdk';
3
3
  import type { Signer, SignatureScheme } from '../types.js';
4
4
  import { bytesToHex } from '../utils/encoding.js';
5
5
  import { AuthDigest } from '../utils/digest.js';
6
6
  import { lookupAuthDigest } from '../lookupAuth.js';
7
+ import { sessionGrantDigest, sessionRevokeAllDigest } from '../session/grant.js';
7
8
 
8
9
  export class FalconSigner implements Signer {
9
10
  readonly commitment: string;
@@ -50,6 +51,16 @@ export class FalconSigner implements Signer {
50
51
  return this.signWord(digest);
51
52
  }
52
53
 
54
+ /** Sign a session grant as its raw RPO digest. */
55
+ async signSessionGrant(grant: SessionGrantFields): Promise<string> {
56
+ return this.signWord(sessionGrantDigest(grant));
57
+ }
58
+
59
+ /** Sign a session revoke-all as its raw RPO digest. */
60
+ async signSessionRevokeAll(signerCommitment: string, timestampMs: number): Promise<string> {
61
+ return this.signWord(sessionRevokeAllDigest(signerCommitment, timestampMs));
62
+ }
63
+
53
64
  async bindAccountKey(midenClient: MidenClient, accountId: string): Promise<void> {
54
65
  const targetAccountId = AccountId.fromHex(accountId);
55
66
  const existingAccountId = await midenClient.keystore.getAccountId(this.publicKeyCommitment);
@@ -1,14 +1,17 @@
1
1
  import { secp256k1 } from '@noble/curves/secp256k1';
2
2
  import { keccak_256 } from '@noble/hashes/sha3.js';
3
- import type { RequestAuthPayload, Signer } from '@openzeppelin/guardian-client';
3
+ import type { RequestAuthPayload, SessionGrantFields, Signer } from '@openzeppelin/guardian-client';
4
4
  import { AuthDigest } from '../utils/digest.js';
5
5
  import { lookupAuthDigest } from '../lookupAuth.js';
6
6
  import { EcdsaFormat } from '../utils/ecdsa.js';
7
7
  import { bytesToHex, hexToBytes } from '../utils/encoding.js';
8
8
  import {
9
+ type GuardianTypedData,
9
10
  guardianKeyDiscoveryTypedData,
10
11
  guardianLookupTypedData,
11
12
  guardianRequestTypedData,
13
+ guardianSessionRevokeAllTypedData,
14
+ guardianSessionTypedData,
12
15
  midenTransactionTypedData,
13
16
  typedDataDigest,
14
17
  } from '../utils/eip712.js';
@@ -99,7 +102,21 @@ export class Eip712Signer implements Signer {
99
102
  return this.signTypedData(midenTransactionTypedData(wordToBytes(commitment)));
100
103
  }
101
104
 
102
- private async signTypedData(data: ReturnType<typeof guardianRequestTypedData>): Promise<string> {
105
+ /**
106
+ * Sign a session grant as readable `GuardianSession` typed
107
+ * data, so the device shows the Guardian, network and expiry rather than a
108
+ * hash. Devices without registered display metadata need "Verbose EIP-712".
109
+ */
110
+ async signSessionGrant(grant: SessionGrantFields): Promise<string> {
111
+ return this.signTypedData(guardianSessionTypedData(grant));
112
+ }
113
+
114
+ /** Sign a session revoke-all as readable `GuardianSessionRevokeAll` typed data. */
115
+ async signSessionRevokeAll(signerCommitment: string, timestampMs: number): Promise<string> {
116
+ return this.signTypedData(guardianSessionRevokeAllTypedData(signerCommitment, timestampMs));
117
+ }
118
+
119
+ private async signTypedData(data: GuardianTypedData): Promise<string> {
103
120
  const result = await this.provider.request({
104
121
  method: 'eth_signTypedData_v4',
105
122
  params: [this.address, JSON.stringify(data)],
@@ -1,9 +1,10 @@
1
- import type { RequestAuthPayload } from '@openzeppelin/guardian-client';
1
+ import type { RequestAuthPayload, SessionGrantFields } from '@openzeppelin/guardian-client';
2
2
  import type { Signer, SignatureScheme } from '../types.js';
3
3
  import { AuthDigest } from '../utils/digest.js';
4
4
  import { EcdsaFormat } from '../utils/ecdsa.js';
5
5
  import { bytesToHex, normalizeHexWord } from '../utils/encoding.js';
6
6
  import { lookupAuthDigest } from '../lookupAuth.js';
7
+ import { sessionGrantDigest, sessionRevokeAllDigest } from '../session/grant.js';
7
8
  import { tryComputeEcdsaCommitmentHex } from '../utils/signature.js';
8
9
  import { wordToBytes } from '../utils/word.js';
9
10
 
@@ -128,6 +129,14 @@ export class MidenWalletSigner implements Signer {
128
129
  return candidate;
129
130
  }
130
131
 
132
+ /**
133
+ * The delegate `localAuthSigner` signs request auth, session grants and
134
+ * revoke-all, so Guardian must read its signatures in its format.
135
+ */
136
+ get requestAuthFormat(): 'eip712' | undefined {
137
+ return this.localAuthSigner?.requestAuthFormat;
138
+ }
139
+
131
140
  async signAccountIdWithTimestamp(accountId: string, timestamp: number): Promise<string> {
132
141
  if (this.localAuthSigner) {
133
142
  return this.localAuthSigner.signAccountIdWithTimestamp(accountId, timestamp);
@@ -164,6 +173,25 @@ export class MidenWalletSigner implements Signer {
164
173
  return this.signWord(digest);
165
174
  }
166
175
 
176
+ /**
177
+ * Sign a session grant. Follows the same signer as request
178
+ * authentication, so the session acts for the key Guardian already knows.
179
+ */
180
+ async signSessionGrant(grant: SessionGrantFields): Promise<string> {
181
+ if (this.localAuthSigner?.signSessionGrant) {
182
+ return this.localAuthSigner.signSessionGrant(grant);
183
+ }
184
+ return this.signWord(sessionGrantDigest(grant));
185
+ }
186
+
187
+ /** Sign a session revoke-all with the same signer as request authentication. */
188
+ async signSessionRevokeAll(signerCommitment: string, timestampMs: number): Promise<string> {
189
+ if (this.localAuthSigner?.signSessionRevokeAll) {
190
+ return this.localAuthSigner.signSessionRevokeAll(signerCommitment, timestampMs);
191
+ }
192
+ return this.signWord(sessionRevokeAllDigest(signerCommitment, timestampMs));
193
+ }
194
+
167
195
  private async signWord(word: { toFelts: () => Array<{ asInt: () => bigint }> }): Promise<string> {
168
196
  const bytes = wordToBytes(word);
169
197
  const signatureBytes = await this.wallet.signBytes(bytes, 'word');
@@ -1,9 +1,10 @@
1
- import type { RequestAuthPayload } from '@openzeppelin/guardian-client';
1
+ import type { RequestAuthPayload, SessionGrantFields } from '@openzeppelin/guardian-client';
2
2
  import type { Signer, SignatureScheme } from '../types.js';
3
3
  import { AuthDigest } from '../utils/digest.js';
4
4
  import { EcdsaFormat } from '../utils/ecdsa.js';
5
5
  import { hexToBytes, uint8ArrayToBase64 } from '../utils/encoding.js';
6
6
  import { lookupAuthDigest } from '../lookupAuth.js';
7
+ import { sessionGrantDigest, sessionRevokeAllDigest } from '../session/grant.js';
7
8
  import { wordToBytes } from '../utils/word.js';
8
9
 
9
10
  export interface ParaSigningContext {
@@ -69,6 +70,16 @@ export class ParaSigner implements Signer {
69
70
  return this.signWord(digest);
70
71
  }
71
72
 
73
+ /** Sign a session grant as its raw RPO digest. */
74
+ async signSessionGrant(grant: SessionGrantFields): Promise<string> {
75
+ return this.signWord(sessionGrantDigest(grant));
76
+ }
77
+
78
+ /** Sign a session revoke-all as its raw RPO digest. */
79
+ async signSessionRevokeAll(signerCommitment: string, timestampMs: number): Promise<string> {
80
+ return this.signWord(sessionRevokeAllDigest(signerCommitment, timestampMs));
81
+ }
82
+
72
83
  private async signWord(word: { toHex: () => string; toFelts: () => Array<{ asInt: () => bigint }> }): Promise<string> {
73
84
  const messageBase64 = uint8ArrayToBase64(
74
85
  hexToBytes(EcdsaFormat.keccakDigestHex(wordToBytes(word))),
@@ -36,8 +36,8 @@ export interface MultisigRequestDraft {
36
36
  * The salt is `options.salt` or a fresh one. `approvalExpirationDelta` left
37
37
  * out means the approval never expires, the upstream default. `boundBlockNum`
38
38
  * left out binds the store's sync height, which is what a proposer wants; a
39
- * rebuild pins the proposal's anchor block and the expiration the summary
40
- * already binds.
39
+ * rebuild pins the block the proposal's summary binds and the expiration the
40
+ * summary already binds.
41
41
  *
42
42
  * The salt is moved across the WASM boundary, so a handle is built here from
43
43
  * the hex rather than taken from the caller, and nothing frees it afterwards.