Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fullfabric/public-api",
"version": "1.7.0",
"version": "1.7.1",
"description": "Function wrappers for the FullFabric public API.",
"type": "module",
"browser": "dist/index.js",
Expand Down
64 changes: 64 additions & 0 deletions spec/utils/getPageDigest.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import getPageDigest from '../../src/utils/getPageDigest'

describe('utils.getPageDigest(opts)', () => {
let oldUserAgent

beforeAll(() => {
oldUserAgent = navigator.userAgent

Object.defineProperty(navigator, 'userAgent', {
configurable: true,
value:
'Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/20.0.3'
})
})

afterAll(() => {
Object.defineProperty(navigator, 'userAgent', {
configurable: true,
value: oldUserAgent
})
})

describe('when no request-id meta element is available', () => {
it('returns null', async () => {
expect(await getPageDigest()).toBeNull()
})

it('can use a given requestId', async () => {
expect(await getPageDigest({ requestId: '12345' })).toEqual(
'7f1c12a5c58c6f29bcaeaf5c6382fef305660da38f2d0919deac9fc4a58705d4'
)
})
})

describe('when a request-id meta element is available', () => {
beforeEach(() => {
document.head.innerHTML = `
<meta name="request-id" content="12345">
`
})

it("uses the request-id meta element's content", async () => {
expect(await getPageDigest()).toEqual(
'7f1c12a5c58c6f29bcaeaf5c6382fef305660da38f2d0919deac9fc4a58705d4'
)
})

it('can use a given requestId instead', async () => {
expect(await getPageDigest({ requestId: '54321' })).toEqual(
'0b3adfbef9370498eb8a03f99fba9e86a41b89d1beef6499646d20090db0739b'
)
})

it("can suffix the digest with another digest of a source's type and ID", async () => {
expect(
await getPageDigest({ sourceType: 'form', sourceId: '12345' })
).toEqual(
'7f1c12a5c58c6f29bcaeaf5c6382fef305660da38f2d0919deac9fc4a58705d4' +
':' +
'0daba4f5e1a46e9e2be8bd4aac8651a020267a7da81690be4f093abe635b6aa1'
)
})
})
})
44 changes: 44 additions & 0 deletions src/utils/getPageDigest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Generates a digest for anti-spam purposes.
*
* This algorithm should match the backend's exactly, otherwise the requests
* will be unauthorized.
*
* By default, each digest is unique to a page load. When there are multiple
* submissions possible in the same page, e.g. via multiple forms, a source can
* be passed to generate a digest unique to it.
*
* @param {String} [requestId] ID of the request, pass if originating from an API request.
* @param {String} [sourceType] Type of source for the digest, should match backend.
* @param {String} [sourceId] ID of source for the digest, should match backend.
* @returns {String} The generated digest.
*/
export default async function getPageDigest({
requestId,
sourceType,
sourceId
} = {}) {
requestId ||= document.querySelector("meta[name='request-id']")?.content || ''

// If there's no requestId, no point generating a digest, won't ever match
// the backend. Same if we can't access the crypto API.
if (!requestId || !crypto?.subtle?.digest) return null

let digest = await makeDigest(`${navigator.userAgent}:${requestId}`)

if (sourceType && sourceId) {
const suffix = `${sourceType}:${sourceId}`
digest += `:${await makeDigest(suffix)}`
}

return digest
}

async function makeDigest(str) {
const message = new TextEncoder().encode(str)
const digestRaw = await crypto.subtle.digest('SHA-256', message)

return Array.from(new Uint8Array(digestRaw))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}
1 change: 1 addition & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as getPageDigest } from './getPageDigest'
export { default as checkResponse } from './checkResponse'
export { default as extractHeaders } from './extractHeaders'
export { default as url } from './url'