Skip to content

Instantly share code, notes, and snippets.

@transitive-bullshit
Created November 7, 2024 19:14
Show Gist options
  • Save transitive-bullshit/df92c7df2b8b1a17a71e0edb20b0cecc to your computer and use it in GitHub Desktop.
Save transitive-bullshit/df92c7df2b8b1a17a71e0edb20b0cecc to your computer and use it in GitHub Desktop.
Get the hash of an object
import decircular from 'decircular'
import isObject from 'is-obj'
import sortKeys from 'sort-keys'
import { type LiteralUnion } from 'type-fest'
export type Encoding = 'hex' | 'base64' | 'buffer'
export type Algorithm = LiteralUnion<'sha1' | 'sha256' | 'sha512', string>
export type Options = {
/**
* The encoding of the returned hash.
*
* @default 'hex'
*/
readonly encoding?: Encoding
/**
* _Don't use `'md5'` or `'sha1'` for anything sensitive. [They're insecure.](http://googleonlinesecurity.blogspot.no/2014/09/gradually-sunsetting-sha-1.html)_
*
* @default 'sha512'
*/
readonly algorithm?: Algorithm
}
export type BufferOptions = {
readonly encoding: 'buffer'
} & Options
function normalizeObject<T>(object: T): T {
if (typeof object === 'string') {
return object.normalize('NFD') as T
}
if (Array.isArray(object)) {
return object.map((element: any) => normalizeObject(element)) as T
}
if (isObject(object)) {
return Object.fromEntries(
Object.entries(object).map(([key, value]) => [
key.normalize('NFD'),
normalizeObject(value)
])
) as T
}
return object
}
export default async function hashObject(
object: Record<string, any>,
{ encoding = 'hex', algorithm = 'sha512' }: BufferOptions | Options = {}
): Promise<string | Uint8Array> {
if (!isObject(object)) {
throw new TypeError('Expected an object')
}
const normalizedObject = normalizeObject(decircular(object))
const stableJsonInput = JSON.stringify(
sortKeys(normalizedObject, { deep: true })
)
const encoder = new TextEncoder()
const data = encoder.encode(stableJsonInput)
const algorithmWebCrypto =
algorithm === 'sha1'
? 'SHA-1'
: algorithm === 'sha256'
? 'SHA-256'
: 'SHA-512'
const hashBuffer = await crypto.subtle.digest(algorithmWebCrypto, data)
if (encoding === 'buffer') {
return new Uint8Array(hashBuffer)
}
const hashArray = Array.from(new Uint8Array(hashBuffer))
if (encoding === 'hex') {
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
} else if (encoding === 'base64') {
// eslint-disable-next-line unicorn/prefer-code-point
return btoa(String.fromCharCode(...hashArray))
} else {
throw new Error(`Unsupported encoding: ${encoding}`)
}
}
@transitive-bullshit
Copy link
Author

Slight modification to https://github.com/sindresorhus/hash-object to support non-node.js environments using the web crypto API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment