Skip to content

Commit bdfa550

Browse files
committed
chore: supporting more types for the nonce caveat
1 parent ffb21fc commit bdfa550

3 files changed

Lines changed: 265 additions & 24 deletions

File tree

packages/delegation-core/src/caveats/nonce.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { isHexString } from '@metamask/utils';
22
import type { BytesLike } from '@metamask/utils';
33

44
import {
5+
bytesLikeToHex,
56
defaultOptions,
67
prepareResult,
78
type EncodingOptions,
@@ -16,7 +17,7 @@ const MAX_NONCE_STRING_LENGTH = 66;
1617
* Terms for configuring a Nonce caveat.
1718
*/
1819
export type NonceTerms = {
19-
/** The nonce as a hex string to allow bulk revocation of delegations. */
20+
/** The nonce as BytesLike (0x-prefixed hex string or Uint8Array) to allow bulk revocation of delegations. */
2021
nonce: BytesLike;
2122
};
2223

@@ -41,33 +42,43 @@ export function createNonceTerms(
4142
*
4243
* @param terms - The terms for the Nonce caveat.
4344
* @param encodingOptions - The encoding options for the result.
44-
* @returns The terms as a 32-byte hex string.
45-
* @throws Error if the nonce is invalid.
45+
* @returns The terms as a 32-byte padded value in the specified encoding format.
46+
* @throws Error if the nonce is invalid or empty.
4647
*/
4748
export function createNonceTerms(
4849
terms: NonceTerms,
4950
encodingOptions: EncodingOptions<ResultValue> = defaultOptions,
5051
): Hex | Uint8Array {
5152
const { nonce } = terms;
5253

53-
if (!nonce || nonce === '0x') {
54-
throw new Error('Invalid nonce: must be a non-empty hex string');
54+
// Handle zero-length Uint8Array specifically
55+
if (nonce instanceof Uint8Array && nonce.length === 0) {
56+
throw new Error('Invalid nonce: Uint8Array must not be empty');
5557
}
5658

57-
if (typeof nonce !== 'string' || !nonce.startsWith('0x')) {
58-
throw new Error('Invalid nonce: must be a valid hex string');
59+
// Validate that strings have 0x prefix (as required by BytesLike)
60+
if (typeof nonce === 'string' && !nonce.startsWith('0x')) {
61+
throw new Error('Invalid nonce: string must have 0x prefix');
62+
}
63+
64+
// Convert to hex string for consistent processing
65+
const hexNonce = bytesLikeToHex(nonce);
66+
67+
// Check for empty hex string (0x) first - more specific error
68+
if (hexNonce === '0x') {
69+
throw new Error('Invalid nonce: must not be empty');
5970
}
6071

61-
if (!isHexString(nonce)) {
62-
throw new Error('Invalid nonce: must be a valid hex string');
72+
if (!isHexString(hexNonce)) {
73+
throw new Error('Invalid nonce: must be a valid BytesLike value');
6374
}
6475

65-
if (nonce.length > MAX_NONCE_STRING_LENGTH) {
76+
if (hexNonce.length > MAX_NONCE_STRING_LENGTH) {
6677
throw new Error('Invalid nonce: must be 32 bytes or less in length');
6778
}
6879

6980
// Remove '0x' prefix for padding, then add it back
70-
const nonceWithoutPrefix = nonce.slice(2);
81+
const nonceWithoutPrefix = hexNonce.slice(2);
7182
const paddedNonce = nonceWithoutPrefix.padStart(64, '0'); // 64 hex chars = 32 bytes
7283
const hexValue = `0x${paddedNonce}`;
7384

packages/delegation-core/test/caveats/nonce.test.ts

Lines changed: 240 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,43 +71,43 @@ describe('createNonceTerms', () => {
7171
const nonce = '0x';
7272

7373
expect(() => createNonceTerms({ nonce })).toThrow(
74-
'Invalid nonce: must be a non-empty hex string',
74+
'Invalid nonce: must not be empty',
7575
);
7676
});
7777

7878
it('throws an error for undefined nonce', () => {
7979
expect(() => createNonceTerms({ nonce: undefined as any })).toThrow(
80-
'Invalid nonce: must be a non-empty hex string',
80+
'Value must be a Uint8Array',
8181
);
8282
});
8383

8484
it('throws an error for null nonce', () => {
8585
expect(() => createNonceTerms({ nonce: null as any })).toThrow(
86-
'Invalid nonce: must be a non-empty hex string',
86+
'Value must be a Uint8Array',
8787
);
8888
});
8989

90-
it('throws an error for nonce without 0x prefix', () => {
90+
it('throws an error for hex nonce without 0x prefix', () => {
9191
const nonce = '1234567890abcdef' as any;
9292

9393
expect(() => createNonceTerms({ nonce })).toThrow(
94-
'Invalid nonce: must be a valid hex string',
94+
'Invalid nonce: string must have 0x prefix',
9595
);
9696
});
9797

9898
it('throws an error for invalid hex characters', () => {
9999
const nonce = '0x1234567890abcdefg' as any;
100100

101101
expect(() => createNonceTerms({ nonce })).toThrow(
102-
'Invalid nonce: must be a valid hex string',
102+
'Invalid nonce: must be a valid BytesLike value',
103103
);
104104
});
105105

106-
it('throws an error for non-string nonce', () => {
106+
it('throws an error for non-BytesLike nonce', () => {
107107
const nonce = 123456 as any;
108108

109109
expect(() => createNonceTerms({ nonce })).toThrow(
110-
'Invalid nonce: must be a valid hex string',
110+
'Value must be a Uint8Array',
111111
);
112112
});
113113

@@ -140,8 +140,80 @@ describe('createNonceTerms', () => {
140140
);
141141
});
142142

143-
// Tests for bytes return type
144-
describe('bytes return type', () => {
143+
// Tests for Uint8Array inputs
144+
describe('Uint8Array inputs', () => {
145+
it('creates valid terms for simple Uint8Array nonce', () => {
146+
const nonce = new Uint8Array([
147+
0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef,
148+
]);
149+
const result = createNonceTerms({ nonce });
150+
151+
expect(result).toStrictEqual(
152+
'0x0000000000000000000000000000000000000000000000001234567890abcdef',
153+
);
154+
});
155+
156+
it('creates valid terms for single byte Uint8Array', () => {
157+
const nonce = new Uint8Array([0x42]);
158+
const result = createNonceTerms({ nonce });
159+
160+
expect(result).toStrictEqual(
161+
'0x0000000000000000000000000000000000000000000000000000000000000042',
162+
);
163+
});
164+
165+
it('creates valid terms for full 32-byte Uint8Array', () => {
166+
const nonce = new Uint8Array([
167+
0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78,
168+
0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef,
169+
0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef,
170+
]);
171+
const result = createNonceTerms({ nonce });
172+
173+
expect(result).toStrictEqual(
174+
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
175+
);
176+
});
177+
178+
it('creates valid terms for zero-filled Uint8Array', () => {
179+
const nonce = new Uint8Array([0x00, 0x00, 0x00, 0x01]);
180+
const result = createNonceTerms({ nonce });
181+
182+
expect(result).toStrictEqual(
183+
'0x0000000000000000000000000000000000000000000000000000000000000001',
184+
);
185+
});
186+
187+
it('throws an error for empty Uint8Array', () => {
188+
const nonce = new Uint8Array([]);
189+
190+
expect(() => createNonceTerms({ nonce })).toThrow(
191+
'Invalid nonce: Uint8Array must not be empty',
192+
);
193+
});
194+
195+
it('throws an error for Uint8Array longer than 32 bytes', () => {
196+
const nonce = new Uint8Array(33).fill(0x42); // 33 bytes
197+
198+
expect(() => createNonceTerms({ nonce })).toThrow(
199+
'Invalid nonce: must be 32 bytes or less in length',
200+
);
201+
});
202+
203+
it('returns Uint8Array when bytes encoding is specified', () => {
204+
const nonce = new Uint8Array([0x12, 0x34, 0x56, 0x78]);
205+
const result = createNonceTerms({ nonce }, { out: 'bytes' });
206+
207+
expect(result).toBeInstanceOf(Uint8Array);
208+
expect(result).toHaveLength(EXPECTED_BYTE_LENGTH);
209+
// Check the last 4 bytes contain our input
210+
const resultArray = Array.from(result);
211+
expect(resultArray.slice(-4)).toEqual([0x12, 0x34, 0x56, 0x78]);
212+
});
213+
});
214+
215+
// Tests for hex string inputs with bytes return type
216+
describe('hex string bytes return type', () => {
145217
it('returns Uint8Array when bytes encoding is specified', () => {
146218
const nonce = '0x1234567890abcdef';
147219
const result = createNonceTerms({ nonce }, { out: 'bytes' });
@@ -201,4 +273,162 @@ describe('createNonceTerms', () => {
201273
expect(Array.from(result)).toEqual(expectedBytes);
202274
});
203275
});
276+
277+
// Tests for edge cases and additional validation
278+
describe('edge cases and additional validation', () => {
279+
it('handles mixed case hex strings correctly', () => {
280+
const nonce = '0xaBcDeF123456';
281+
const result = createNonceTerms({ nonce });
282+
283+
expect(result).toStrictEqual(
284+
'0x0000000000000000000000000000000000000000000000000000aBcDeF123456',
285+
);
286+
});
287+
288+
it('handles different BytesLike types consistently', () => {
289+
// Same data in different formats should produce same result
290+
const hexNonce = '0x123456789abcdef0';
291+
const uint8Nonce = new Uint8Array([
292+
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
293+
]);
294+
295+
const hexResult = createNonceTerms({ nonce: hexNonce });
296+
const uint8Result = createNonceTerms({ nonce: uint8Nonce });
297+
298+
expect(hexResult).toStrictEqual(uint8Result);
299+
});
300+
301+
it('handles very small values correctly', () => {
302+
const hexNonce = '0x01';
303+
const uint8Nonce = new Uint8Array([0x01]);
304+
305+
const hexResult = createNonceTerms({ nonce: hexNonce });
306+
const uint8Result = createNonceTerms({ nonce: uint8Nonce });
307+
308+
const expected =
309+
'0x0000000000000000000000000000000000000000000000000000000000000001';
310+
expect(hexResult).toStrictEqual(expected);
311+
expect(uint8Result).toStrictEqual(expected);
312+
});
313+
314+
it('handles maximum size values correctly', () => {
315+
const maxBytes = new Array(32).fill(0xff);
316+
const hexNonce = `0x${maxBytes.map((b) => b.toString(16)).join('')}`;
317+
const uint8Nonce = new Uint8Array(maxBytes);
318+
319+
const hexResult = createNonceTerms({ nonce: hexNonce as `0x${string}` });
320+
const uint8Result = createNonceTerms({ nonce: uint8Nonce });
321+
322+
const expected =
323+
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
324+
expect(hexResult).toStrictEqual(expected);
325+
expect(uint8Result).toStrictEqual(expected);
326+
});
327+
328+
it('handles boolean false correctly', () => {
329+
expect(() => createNonceTerms({ nonce: false as any })).toThrow(
330+
'Value must be a Uint8Array',
331+
);
332+
});
333+
334+
it('handles empty object correctly', () => {
335+
expect(() => createNonceTerms({ nonce: {} as any })).toThrow(
336+
'Value must be a Uint8Array',
337+
);
338+
});
339+
340+
it('handles empty array correctly', () => {
341+
expect(() => createNonceTerms({ nonce: [] as any })).toThrow(
342+
'Value must be a Uint8Array',
343+
);
344+
});
345+
346+
it('handles string with only 0x prefix correctly', () => {
347+
const nonce = '0x';
348+
expect(() => createNonceTerms({ nonce })).toThrow(
349+
'Invalid nonce: must not be empty',
350+
);
351+
});
352+
353+
it('handles non-hex string correctly', () => {
354+
expect(() =>
355+
createNonceTerms({ nonce: 'not-hex-string' as any }),
356+
).toThrow('Invalid nonce: string must have 0x prefix');
357+
});
358+
359+
it('handles hex string with invalid characters correctly', () => {
360+
expect(() => createNonceTerms({ nonce: '0x123g' as any })).toThrow(
361+
'Invalid nonce: must be a valid BytesLike value',
362+
);
363+
});
364+
365+
it('validates specific error message for Uint8Array empty case', () => {
366+
const nonce = new Uint8Array([]);
367+
expect(() => createNonceTerms({ nonce })).toThrow(
368+
'Invalid nonce: Uint8Array must not be empty',
369+
);
370+
});
371+
372+
it('validates specific error message for oversized input', () => {
373+
const nonce = new Uint8Array(33).fill(0xff);
374+
expect(() => createNonceTerms({ nonce })).toThrow(
375+
'Invalid nonce: must be 32 bytes or less in length',
376+
);
377+
});
378+
379+
it('handles zero byte values correctly', () => {
380+
const nonce = new Uint8Array([0x00]);
381+
const result = createNonceTerms({ nonce });
382+
expect(result).toStrictEqual(
383+
'0x0000000000000000000000000000000000000000000000000000000000000000',
384+
);
385+
});
386+
387+
it('handles maximum valid single byte value correctly', () => {
388+
const nonce = new Uint8Array([0xff]);
389+
const result = createNonceTerms({ nonce });
390+
expect(result).toStrictEqual(
391+
'0x00000000000000000000000000000000000000000000000000000000000000ff',
392+
);
393+
});
394+
395+
it('preserves exact byte order for full-size inputs', () => {
396+
const bytes = Array.from({ length: 32 }, (_, i) => i % 256);
397+
const nonce = new Uint8Array(bytes);
398+
const result = createNonceTerms({ nonce });
399+
400+
// Should preserve exact byte order without padding
401+
const expectedHex = `0x${bytes
402+
.map((b) => b.toString(16).padStart(2, '0'))
403+
.join('')}`;
404+
expect(result).toStrictEqual(expectedHex);
405+
});
406+
407+
it('handles very large hex strings correctly', () => {
408+
// Test exactly at the 32-byte boundary
409+
const maxHex = `0x${'ff'.repeat(32)}`;
410+
const result = createNonceTerms({ nonce: maxHex as `0x${string}` });
411+
expect(result).toStrictEqual(maxHex);
412+
});
413+
414+
it('handles odd-length hex strings by padding correctly', () => {
415+
const nonce = '0x123';
416+
const result = createNonceTerms({ nonce });
417+
expect(result).toStrictEqual(
418+
'0x0000000000000000000000000000000000000000000000000000000000000123',
419+
);
420+
});
421+
422+
it('distinguishes between empty nonce and invalid hex characters', () => {
423+
// Empty nonce gets specific "must not be empty" error
424+
expect(() => createNonceTerms({ nonce: '0x' })).toThrow(
425+
'Invalid nonce: must not be empty',
426+
);
427+
428+
// Invalid hex characters get "must be a valid BytesLike value" error
429+
expect(() => createNonceTerms({ nonce: '0x123g' as any })).toThrow(
430+
'Invalid nonce: must be a valid BytesLike value',
431+
);
432+
});
433+
});
204434
});

packages/delegation-toolkit/test/caveatBuilder/nonceBuilder.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,19 @@ describe('nonceBuilder()', () => {
2020
describe('validation', () => {
2121
it('should fail with an empty nonce', () => {
2222
expect(() => buildWithNonce('0x')).to.throw(
23-
'Invalid nonce: must be a non-empty hex string',
23+
'Invalid nonce: must not be empty',
2424
);
2525
});
2626

2727
it('should fail with a null nonce', () => {
2828
expect(() => buildWithNonce(null as any as Hex)).to.throw(
29-
'Invalid nonce: must be a non-empty hex string',
29+
'Value must be a Uint8Array',
3030
);
3131
});
3232

3333
it('should fail with an invalid hex string', () => {
3434
expect(() => buildWithNonce('0xinvalid' as Hex)).to.throw(
35-
'Invalid nonce: must be a valid hex string',
35+
'Invalid nonce: must be a valid BytesLike value',
3636
);
3737
});
3838

0 commit comments

Comments
 (0)