diff --git a/.changeset/old-cameras-unite.md b/.changeset/old-cameras-unite.md new file mode 100644 index 000000000..0f0b1ae6a --- /dev/null +++ b/.changeset/old-cameras-unite.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-next': patch +--- + +Added support for nonce attribute in injected load script diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index 32801d93c..2eef95d8d 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -214,11 +214,7 @@ async function registerPlugins( await import( /* webpackChunkName: "remoteMiddleware" */ '../plugins/remote-middleware' ).then(async ({ remoteMiddlewares }) => { - const middleware = await remoteMiddlewares( - ctx, - cdnSettings, - options.obfuscate - ) + const middleware = await remoteMiddlewares(ctx, cdnSettings, options) const promises = middleware.map((mdw) => analytics.addSourceMiddleware(mdw) ) diff --git a/packages/browser/src/browser/settings.ts b/packages/browser/src/browser/settings.ts index 0c92bed9a..b908a1c4b 100644 --- a/packages/browser/src/browser/settings.ts +++ b/packages/browser/src/browser/settings.ts @@ -224,6 +224,10 @@ export interface InitOptions { plan?: Plan retryQueue?: boolean obfuscate?: boolean + /** + * Nonce to be used by the injected segment script + */ + nonce?: string /** * This callback allows you to update/mutate CDN Settings. * This is called directly after settings are fetched from the CDN. diff --git a/packages/browser/src/browser/standalone.ts b/packages/browser/src/browser/standalone.ts index 86d6346de..df994eeaf 100644 --- a/packages/browser/src/browser/standalone.ts +++ b/packages/browser/src/browser/standalone.ts @@ -33,7 +33,7 @@ import { setGlobalAnalyticsKey } from '../lib/global-analytics-helper' let ajsIdentifiedCSP = false const sendErrorMetrics = (tags: string[]) => { - // this should not be instantied at the root, or it will break ie11. + // this should not be instantiated at the root, or it will break ie11. const metrics = new RemoteMetrics() metrics.increment('analytics_js.invoke.error', [ ...tags, diff --git a/packages/browser/src/plugins/ajs-destination/index.ts b/packages/browser/src/plugins/ajs-destination/index.ts index b8340a11e..04f9884cb 100644 --- a/packages/browser/src/plugins/ajs-destination/index.ts +++ b/packages/browser/src/plugins/ajs-destination/index.ts @@ -136,12 +136,7 @@ export class LegacyDestination implements InternalPluginWithAddMiddleware { const integrationSource = this.integrationSource ?? - (await loadIntegration( - ctx, - this.name, - this.version, - this.options.obfuscate - )) + (await loadIntegration(ctx, this.name, this.version, this.options)) this.integration = buildIntegration( integrationSource, diff --git a/packages/browser/src/plugins/ajs-destination/loader.ts b/packages/browser/src/plugins/ajs-destination/loader.ts index c8668f4d1..efa737dea 100644 --- a/packages/browser/src/plugins/ajs-destination/loader.ts +++ b/packages/browser/src/plugins/ajs-destination/loader.ts @@ -71,10 +71,11 @@ export async function loadIntegration( ctx: Context, name: string, version: string, - obfuscate?: boolean + options?: { obfuscate?: boolean; nonce?: string } ): Promise { + const nonceAttr = options?.nonce ? { nonce: options.nonce } : undefined const pathName = normalizeName(name) - const obfuscatedPathName = obfuscatePathName(pathName, obfuscate) + const obfuscatedPathName = obfuscatePathName(pathName, options?.obfuscate) const path = getNextIntegrationsURL() const fullPath = `${path}/integrations/${ @@ -82,7 +83,7 @@ export async function loadIntegration( }/${version}/${obfuscatedPathName ?? pathName}.dynamic.js.gz` try { - await loadScript(fullPath) + await loadScript(fullPath, nonceAttr) recordLoadMetrics(fullPath, ctx, name) } catch (err) { ctx.stats.gauge('legacy_destination_time', -1, [`plugin:${name}`, `failed`]) @@ -91,7 +92,9 @@ export async function loadIntegration( // @ts-ignore const deps: string[] = window[`${pathName}Deps`] - await Promise.all(deps.map((dep) => loadScript(path + dep + '.gz'))) + await Promise.all( + deps.map((dep) => loadScript(path + dep + '.gz', nonceAttr)) + ) // @ts-ignore window[`${pathName}Loader`]() diff --git a/packages/browser/src/plugins/remote-loader/__tests__/index.test.ts b/packages/browser/src/plugins/remote-loader/__tests__/index.test.ts index 0a8aa9710..6fa86a463 100644 --- a/packages/browser/src/plugins/remote-loader/__tests__/index.test.ts +++ b/packages/browser/src/plugins/remote-loader/__tests__/index.test.ts @@ -42,7 +42,10 @@ describe('Remote Loader', () => { {} ) - expect(loader.loadScript).toHaveBeenCalledWith('cdn/path/to/file.js') + expect(loader.loadScript).toHaveBeenCalledWith( + 'cdn/path/to/file.js', + undefined + ) }) it('should attempt to load a script from the obfuscated url of each remotePlugin', async () => { @@ -69,6 +72,30 @@ describe('Remote Loader', () => { ) }) + it('should pass the nonce attribute for each remotePlugin load', async () => { + await remoteLoader( + { + ...cdnSettingsMinimal, + remotePlugins: [ + { + name: 'remote plugin', + creationName: 'remote plugin', + url: 'cdn/path/to/file.js', + libraryName: 'testPlugin', + settings: {}, + }, + ], + }, + {}, + {}, + { nonce: 'my-foo-nonce' } + ) + + expect(loader.loadScript).toHaveBeenCalledWith('cdn/path/to/file.js', { + nonce: 'my-foo-nonce', + }) + }) + it('should attempt to load a script from a custom CDN', async () => { window.analytics = {} window.analytics._cdn = 'foo.com' @@ -89,7 +116,10 @@ describe('Remote Loader', () => { {} ) - expect(loader.loadScript).toHaveBeenCalledWith('foo.com/actions/file.js') + expect(loader.loadScript).toHaveBeenCalledWith( + 'foo.com/actions/file.js', + undefined + ) }) it('should work if the cdn is staging', async () => { @@ -114,7 +144,10 @@ describe('Remote Loader', () => { {} ) - expect(loader.loadScript).toHaveBeenCalledWith('foo.com/actions/foo.js') + expect(loader.loadScript).toHaveBeenCalledWith( + 'foo.com/actions/foo.js', + undefined + ) }) it('should attempt calling the library', async () => { diff --git a/packages/browser/src/plugins/remote-loader/index.ts b/packages/browser/src/plugins/remote-loader/index.ts index 617272715..ab4442df5 100644 --- a/packages/browser/src/plugins/remote-loader/index.ts +++ b/packages/browser/src/plugins/remote-loader/index.ts @@ -223,13 +223,14 @@ function isPluginDisabled( async function loadPluginFactory( remotePlugin: RemotePlugin, - obfuscate?: boolean + options?: { obfuscate?: boolean; nonce?: string } ): Promise { try { const defaultCdn = new RegExp('https://cdn.segment.(com|build)') const cdn = getCDN() + const nonceAttr = options?.nonce ? { nonce: options.nonce } : undefined - if (obfuscate) { + if (options?.obfuscate) { const urlSplit = remotePlugin.url.split('/') const name = urlSplit[urlSplit.length - 2] const obfuscatedURL = remotePlugin.url.replace( @@ -241,10 +242,10 @@ async function loadPluginFactory( } catch (error) { // Due to syncing concerns it is possible that the obfuscated action destination (or requested version) might not exist. // We should use the unobfuscated version as a fallback. - await loadScript(remotePlugin.url.replace(defaultCdn, cdn)) + await loadScript(remotePlugin.url.replace(defaultCdn, cdn), nonceAttr) } } else { - await loadScript(remotePlugin.url.replace(defaultCdn, cdn)) + await loadScript(remotePlugin.url.replace(defaultCdn, cdn), nonceAttr) } // @ts-expect-error @@ -278,7 +279,7 @@ export async function remoteLoader( const pluginFactory = pluginSources?.find( ({ pluginName }) => pluginName === remotePlugin.name - ) || (await loadPluginFactory(remotePlugin, options?.obfuscate)) + ) || (await loadPluginFactory(remotePlugin, options)) if (pluginFactory) { const intg = mergedIntegrations[remotePlugin.name] diff --git a/packages/browser/src/plugins/remote-middleware/index.ts b/packages/browser/src/plugins/remote-middleware/index.ts index 5ff2ad86d..a3a2b8761 100644 --- a/packages/browser/src/plugins/remote-middleware/index.ts +++ b/packages/browser/src/plugins/remote-middleware/index.ts @@ -8,7 +8,7 @@ import { MiddlewareFunction } from '../middleware' export async function remoteMiddlewares( ctx: Context, settings: CDNSettings, - obfuscate?: boolean + options?: { obfuscate?: boolean; nonce?: string } ): Promise { if (isServer()) { return [] @@ -22,13 +22,14 @@ export async function remoteMiddlewares( const scripts = names.map(async (name) => { const nonNamespaced = name.replace('@segment/', '') let bundleName = nonNamespaced - if (obfuscate) { + if (options?.obfuscate) { bundleName = btoa(nonNamespaced).replace(/=/g, '') } const fullPath = `${path}/middleware/${bundleName}/latest/${bundleName}.js.gz` try { - await loadScript(fullPath) + const nonceAttr = options?.nonce ? { nonce: options.nonce } : undefined + await loadScript(fullPath, nonceAttr) // @ts-ignore return window[`${nonNamespaced}Middleware`] as MiddlewareFunction } catch (error: any) {