Skip to content

Commit

Permalink
feat(siwe): add module
Browse files Browse the repository at this point in the history
  • Loading branch information
tmm committed Aug 20, 2024
1 parent 6f1fe74 commit e96771d
Show file tree
Hide file tree
Showing 20 changed files with 1,321 additions and 0 deletions.
19 changes: 19 additions & 0 deletions src/Siwe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export {
createSiweMessage as createMessage,
createSiweMessage,
} from './internal/siwe/createSiweMessage.js'

export {
generateSiweNonce as generateNonce,
generateSiweNonce,
} from './internal/siwe/generateSiweNonce.js'

export {
parseSiweMessage as parseMessage,
parseSiweMessage,
} from './internal/siwe/parseSiweMessage.js'

export {
validateSiweMessage as validateMessage,
validateSiweMessage,
} from './internal/siwe/validateSiweMessage.js'
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * as Hex from './Hex.js'
export * as Secp256k1 from './Secp256k1.js'
export * as Signature from './Signature.js'
export * as Rlp from './Rlp.js'
export * as Siwe from './Siwe.js'
export * as TypedData from './TypedData.js'
export * as Types from './Types.js'
export * as Value from './Value.js'
26 changes: 26 additions & 0 deletions src/internal/errors/siwe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect, test } from 'vitest'

import { SiweInvalidMessageFieldError } from './siwe.js'

test('SiweInvalidMessageFieldError', () => {
expect(
new SiweInvalidMessageFieldError({
field: 'nonce',
metaMessages: [
'- Nonce must be at least 8 characters.',
'- Nonce must be alphanumeric.',
'',
'Provided value: foobarbaz$',
],
}),
).toMatchInlineSnapshot(`
[SiweInvalidMessageFieldError: Invalid Sign-In with Ethereum message field "nonce".
- Nonce must be at least 8 characters.
- Nonce must be alphanumeric.
Provided value: foobarbaz$
See: https://oxlib.sh/errors#siweinvalidmessagefielderror]
`)
})
16 changes: 16 additions & 0 deletions src/internal/errors/siwe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BaseError } from './base.js'

export class SiweInvalidMessageFieldError extends BaseError {
override readonly name = 'SiweInvalidMessageFieldError'

constructor(parameters: {
field: string
metaMessages?: string[] | undefined
}) {
const { field, metaMessages } = parameters
super(`Invalid Sign-In with Ethereum message field "${field}".`, {
docsPath: '/errors#siweinvalidmessagefielderror',
metaMessages,
})
}
}
42 changes: 42 additions & 0 deletions src/internal/isUri.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect, test } from 'vitest'

import { isUri } from './isUri.js'

test('default', () => {
expect(isUri('https://example.com/foo')).toMatchInlineSnapshot(
`"https://example.com/foo"`,
)
})

test('behavior: check for illegal characters', () => {
expect(isUri('^')).toBeFalsy()
})

test('incomplete hex escapes', () => {
expect(isUri('%$#')).toBeFalsy()
expect(isUri('%0:#')).toBeFalsy()
})

test('missing scheme', () => {
expect(isUri('example.com/foo')).toBeFalsy()
})

test('authority with missing path', () => {
expect(isUri('1http:////foo.html')).toBeFalsy()
})

test('scheme begins with letter', () => {
expect(isUri('$https://example.com/foo')).toBeFalsy()
})

test('query', () => {
expect(isUri('https://example.com/foo?bar')).toMatchInlineSnapshot(
`"https://example.com/foo?bar"`,
)
})

test('fragment', () => {
expect(isUri('https://example.com/foo#bar')).toMatchInlineSnapshot(
`"https://example.com/foo#bar"`,
)
})
52 changes: 52 additions & 0 deletions src/internal/isUri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/** @internal */
export function isUri(value: string) {
// based on https://github.com/ogt/valid-url

// check for illegal characters
if (/[^a-z0-9\:\/\?\#\[\]\@\!\$\&\'\(\)\*\+\,\;\=\.\-\_\~\%]/i.test(value))
return false

// check for hex escapes that aren't complete
if (/%[^0-9a-f]/i.test(value)) return false
if (/%[0-9a-f](:?[^0-9a-f]|$)/i.test(value)) return false

// from RFC 3986
const splitted = splitUri(value)
const scheme = splitted[1]
const authority = splitted[2]
const path = splitted[3]
const query = splitted[4]
const fragment = splitted[5]

// scheme and path are required, though the path can be empty
if (!(scheme?.length && path && path.length >= 0)) return false

// if authority is present, the path must be empty or begin with a /
if (authority?.length) {
if (!(path.length === 0 || /^\//.test(path))) return false
} else {
// if authority is not present, the path must not start with //
if (/^\/\//.test(path)) return false
}

// scheme must begin with a letter, then consist of letters, digits, +, ., or -
if (!/^[a-z][a-z0-9\+\-\.]*$/.test(scheme.toLowerCase())) return false

let out = ''
// re-assemble the URL per section 5.3 in RFC 3986
out += `${scheme}:`
if (authority?.length) out += `//${authority}`

out += path

if (query?.length) out += `?${query}`
if (fragment?.length) out += `#${fragment}`

return out
}

function splitUri(value: string) {
return value.match(
/(?:([^:\/?#]+):)?(?:\/\/([^\/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?/,
)!
}
Loading

0 comments on commit e96771d

Please sign in to comment.