From df6c1737abb50a0899a827f8689d1a25acb5d7ae Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:47:22 +1000 Subject: [PATCH] fix(siwe): parse resources from the resources section --- .changeset/siwe-resources.md | 5 ++ src/core/Siwe.ts | 34 ++++---- src/core/_test/Siwe.test.ts | 147 +++++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 .changeset/siwe-resources.md diff --git a/.changeset/siwe-resources.md b/.changeset/siwe-resources.md new file mode 100644 index 00000000..3ad8cfd4 --- /dev/null +++ b/.changeset/siwe-resources.md @@ -0,0 +1,5 @@ +--- +"ox": patch +--- + +Fixed `Siwe.parseMessage` losing or truncating resources when `Resources:` appeared in another message field or resource. diff --git a/src/core/Siwe.ts b/src/core/Siwe.ts index 9a4d37bb..06f7e09a 100644 --- a/src/core/Siwe.ts +++ b/src/core/Siwe.ts @@ -21,7 +21,7 @@ export const prefixRegex = // https://regexr.com/80gf9 export const suffixRegex = - /(?:URI: (?.+))\n(?:Version: (?.+))\n(?:Chain ID: (?\d+))\n(?:Nonce: (?[a-zA-Z0-9]+))\n(?:Issued At: (?.+))(?:\nExpiration Time: (?.+))?(?:\nNot Before: (?.+))?(?:\nRequest ID: (?.+))?/ + /(?:URI: (?.+))\n(?:Version: (?.+))\n(?:Chain ID: (?\d+))\n(?:Nonce: (?[a-zA-Z0-9]+))\n(?:Issued At: (?.+))(?:\nExpiration Time: (?.+))?(?:\nNot Before: (?.+))?(?:\nRequest ID: (?.*))?(?:\nResources:(?(?:\n- .+)*))?/ const siweDateTimeRegex = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[Tt]([01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/ @@ -421,18 +421,26 @@ export function parseMessage(message: string): ExactPartial { scheme?: string statement?: string } - const { chainId, expirationTime, issuedAt, notBefore, requestId, ...suffix } = - (message.match(suffixRegex)?.groups ?? {}) as { - chainId: string - expirationTime?: string - issuedAt?: string - nonce: string - notBefore?: string - requestId?: string - uri: string - version: '1' - } - const resources = message.split('Resources:')[1]?.split('\n- ').slice(1) + const { + chainId, + expirationTime, + issuedAt, + notBefore, + requestId, + resources: resources_, + ...suffix + } = (message.match(suffixRegex)?.groups ?? {}) as { + chainId: string + expirationTime?: string + issuedAt?: string + nonce: string + notBefore?: string + requestId?: string + resources?: string + uri: string + version: '1' + } + const resources = resources_?.split('\n- ').slice(1) return { ...prefix, ...suffix, diff --git a/src/core/_test/Siwe.test.ts b/src/core/_test/Siwe.test.ts index cb0de39a..24c17fee 100644 --- a/src/core/_test/Siwe.test.ts +++ b/src/core/_test/Siwe.test.ts @@ -773,6 +773,153 @@ Resources: `) }) + test('behavior: "Resources:" in statement', () => { + const message = `example.com wants you to sign in with your Ethereum account: +0xA0Cf798816D4b9b9866b5330EEa46a18382f251e + +See the Resources: list below. + +URI: https://example.com/path +Version: 1 +Chain ID: 1 +Nonce: foobarbaz +Issued At: 2023-02-01T00:00:00.000Z +Resources: +- https://example.com/foo +- https://example.com/bar` + const parsed = Siwe.parseMessage(message) + expect(parsed).toMatchInlineSnapshot(` + { + "address": "0xA0Cf798816D4b9b9866b5330EEa46a18382f251e", + "chainId": 1, + "domain": "example.com", + "issuedAt": 2023-02-01T00:00:00.000Z, + "nonce": "foobarbaz", + "resources": [ + "https://example.com/foo", + "https://example.com/bar", + ], + "statement": "See the Resources: list below.", + "uri": "https://example.com/path", + "version": "1", + } + `) + }) + + test('behavior: "Resources:" in statement without resources', () => { + const message = `example.com wants you to sign in with your Ethereum account: +0xA0Cf798816D4b9b9866b5330EEa46a18382f251e + +See the Resources: page. + +URI: https://example.com/path +Version: 1 +Chain ID: 1 +Nonce: foobarbaz +Issued At: 2023-02-01T00:00:00.000Z` + const parsed = Siwe.parseMessage(message) + expect(parsed.resources).toBeUndefined() + }) + + test('behavior: "Resources:" in uri', () => { + const message = `example.com wants you to sign in with your Ethereum account: +0xA0Cf798816D4b9b9866b5330EEa46a18382f251e + + +URI: https://example.com/Resources:path +Version: 1 +Chain ID: 1 +Nonce: foobarbaz +Issued At: 2023-02-01T00:00:00.000Z +Resources: +- https://example.com/foo` + const parsed = Siwe.parseMessage(message) + expect(parsed.resources).toMatchInlineSnapshot(` + [ + "https://example.com/foo", + ] + `) + }) + + test('behavior: "Resources:" in requestId', () => { + const message = `example.com wants you to sign in with your Ethereum account: +0xA0Cf798816D4b9b9866b5330EEa46a18382f251e + + +URI: https://example.com/path +Version: 1 +Chain ID: 1 +Nonce: foobarbaz +Issued At: 2023-02-01T00:00:00.000Z +Request ID: Resources:123 +Resources: +- https://example.com/foo` + const parsed = Siwe.parseMessage(message) + expect(parsed.resources).toMatchInlineSnapshot(` + [ + "https://example.com/foo", + ] + `) + }) + + test('behavior: "Resources:" in a resource', () => { + const message = `example.com wants you to sign in with your Ethereum account: +0xA0Cf798816D4b9b9866b5330EEa46a18382f251e + + +URI: https://example.com/path +Version: 1 +Chain ID: 1 +Nonce: foobarbaz +Issued At: 2023-02-01T00:00:00.000Z +Resources: +- https://example.com/Resources:foo +- https://example.com/bar` + const parsed = Siwe.parseMessage(message) + expect(parsed.resources).toMatchInlineSnapshot(` + [ + "https://example.com/Resources:foo", + "https://example.com/bar", + ] + `) + }) + + test('behavior: empty requestId with resources', () => { + // EIP-4361 allows an empty Request ID (`request-id = *pchar`). + const message = `example.com wants you to sign in with your Ethereum account: +0xA0Cf798816D4b9b9866b5330EEa46a18382f251e + + +URI: https://example.com/path +Version: 1 +Chain ID: 1 +Nonce: foobarbaz +Issued At: 2023-02-01T00:00:00.000Z +Request ID: \nResources: +- https://example.com/foo` + const parsed = Siwe.parseMessage(message) + expect(parsed.resources).toMatchInlineSnapshot(` + [ + "https://example.com/foo", + ] + `) + }) + + test('behavior: empty resources', () => { + const message = Siwe.createMessage({ + address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + chainId: 1, + domain: 'example.com', + issuedAt: new Date('2023-02-01T00:00:00.000Z'), + nonce: 'foobarbaz', + resources: [], + statement: 'Resources:', + uri: 'https://example.com/path', + version: '1', + }) + expect(Siwe.parseMessage(message).resources).toMatchInlineSnapshot('[]') + }) + test('behavior: no suffix', () => { const message = `https://example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e