diff --git a/package.json b/package.json index c520dd0..e9bb106 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,7 @@ }, "dependencies": { "base64-js": "^1.3.0", - "fast-deep-equal": "^2.0.1", - "uuid": "^8.0.0" + "fast-deep-equal": "^2.0.1" }, "repository": { "type": "git", diff --git a/src/AnonymousContextProcessor.js b/src/AnonymousContextProcessor.js index 8223b5c..ddff8ec 100644 --- a/src/AnonymousContextProcessor.js +++ b/src/AnonymousContextProcessor.js @@ -1,4 +1,4 @@ -const { v1: uuidv1 } = require('uuid'); +const { randomUuidV4 } = require('./uuid'); const { getContextKinds } = require('./context'); const errors = require('./errors'); @@ -57,7 +57,7 @@ function AnonymousContextProcessor(persistentStorage) { context.key = cachedId; return context; } else { - const id = uuidv1(); + const id = randomUuidV4(); context.key = id; return setCachedContextKey(id, kind).then(() => context); } diff --git a/src/EventSender.js b/src/EventSender.js index 5a9a620..b295d4b 100644 --- a/src/EventSender.js +++ b/src/EventSender.js @@ -1,6 +1,6 @@ const errors = require('./errors'); const utils = require('./utils'); -const { v1: uuidv1 } = require('uuid'); +const { randomUuidV4 } = require('./uuid'); const { getLDHeaders, transformHeaders } = require('./headers'); function EventSender(platform, environmentId, options) { @@ -25,7 +25,7 @@ function EventSender(platform, environmentId, options) { } const jsonBody = JSON.stringify(events); - const payloadId = isDiagnostic ? null : uuidv1(); + const payloadId = isDiagnostic ? null : randomUuidV4(); function doPostRequest(canRetry) { const headers = isDiagnostic diff --git a/src/__tests__/diagnosticEvents-test.js b/src/__tests__/diagnosticEvents-test.js index e38d32c..7186538 100644 --- a/src/__tests__/diagnosticEvents-test.js +++ b/src/__tests__/diagnosticEvents-test.js @@ -16,6 +16,12 @@ describe('DiagnosticId', () => { expect(id1.diagnosticId).not.toEqual(id2.diagnosticId); }); + it('generates valid UUID v4 format', () => { + const id = DiagnosticId('key'); + const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + expect(id.diagnosticId).toMatch(uuidV4Regex); + }); + it('uses only last 6 characters of key', () => { const id = DiagnosticId('0123456789abcdef'); expect(id.sdkKeySuffix).toEqual('abcdef'); diff --git a/src/diagnosticEvents.js b/src/diagnosticEvents.js index 4d7ad19..f5949e3 100644 --- a/src/diagnosticEvents.js +++ b/src/diagnosticEvents.js @@ -1,7 +1,4 @@ -const { v1: uuidv1 } = require('uuid'); -// Note that in the diagnostic events spec, these IDs are to be generated with UUID v4. However, -// in JS we were already using v1 for unique context keys, so to avoid bringing in two packages we -// will use v1 here as well. +const { randomUuidV4 } = require('./uuid'); const { baseOptionDefs } = require('./configuration'); const messages = require('./messages'); @@ -9,7 +6,7 @@ const { appendUrlPath } = require('./utils'); function DiagnosticId(sdkKey) { const ret = { - diagnosticId: uuidv1(), + diagnosticId: randomUuidV4(), }; if (sdkKey) { ret.sdkKeySuffix = sdkKey.length > 6 ? sdkKey.substring(sdkKey.length - 6) : sdkKey; diff --git a/src/uuid.js b/src/uuid.js new file mode 100644 index 0000000..821b80c --- /dev/null +++ b/src/uuid.js @@ -0,0 +1,70 @@ +/* global crypto */ +// The implementation in this file generates UUIDs in v4 format and is suitable +// for use as a UUID in LaunchDarkly events. It is not a rigorous implementation. +// +// Adapted from: +// https://github.com/launchdarkly/js-core/blob/main/packages/sdk/browser/src/platform/randomUuidV4.ts + +// It uses crypto.randomUUID when available. +// If crypto.randomUUID is not available, then it uses random values and forms +// the UUID itself. +// When possible it uses crypto.getRandomValues, but it can use Math.random +// if crypto.getRandomValues is not available. + +// UUIDv4 Struct definition. +// https://www.rfc-archive.org/getrfc.php?rfc=4122 +// Appendix A. Appendix A - Sample Implementation +const timeLow = { start: 0, end: 3 }; +const timeMid = { start: 4, end: 5 }; +const timeHiAndVersion = { start: 6, end: 7 }; +const clockSeqHiAndReserved = { start: 8, end: 8 }; +const clockSeqLow = { start: 9, end: 9 }; +const nodes = { start: 10, end: 15 }; + +function getRandom128bit() { + if (typeof crypto !== 'undefined' && crypto.getRandomValues) { + const typedArray = new Uint8Array(16); + crypto.getRandomValues(typedArray); + return [...typedArray.values()]; + } + const values = []; + for (let index = 0; index < 16; index += 1) { + values.push(Math.floor(Math.random() * 256)); + } + return values; +} + +function hex(bytes, range) { + let strVal = ''; + for (let index = range.start; index <= range.end; index += 1) { + strVal += bytes[index].toString(16).padStart(2, '0'); + } + return strVal; +} + +function formatDataAsUuidV4(bytes) { + // eslint-disable-next-line no-bitwise, no-param-reassign + bytes[clockSeqHiAndReserved.start] = (bytes[clockSeqHiAndReserved.start] | 0x80) & 0xbf; + // eslint-disable-next-line no-bitwise, no-param-reassign + bytes[timeHiAndVersion.start] = (bytes[timeHiAndVersion.start] & 0x0f) | 0x40; + + return ( + `${hex(bytes, timeLow)}-${hex(bytes, timeMid)}-${hex(bytes, timeHiAndVersion)}-` + + `${hex(bytes, clockSeqHiAndReserved)}${hex(bytes, clockSeqLow)}-${hex(bytes, nodes)}` + ); +} + +function fallbackUuidV4() { + const bytes = getRandom128bit(); + return formatDataAsUuidV4(bytes); +} + +function randomUuidV4() { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + + return fallbackUuidV4(); +} + +module.exports = { randomUuidV4, fallbackUuidV4, formatDataAsUuidV4 };