Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
blittle committed Aug 17, 2023
1 parent 8f91138 commit 8066aa5
Show file tree
Hide file tree
Showing 9 changed files with 68 additions and 25 deletions.
15 changes: 15 additions & 0 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion packages/hydrogen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
],
"dependencies": {
"@shopify/hydrogen-react": "2023.7.0",
"react": "^18.2.0"
"react": "^18.2.0",
"content-security-policy-builder": "^2.1.1"
},
"peerDependencies": {
"@remix-run/react": "1.19.1",
Expand Down
11 changes: 11 additions & 0 deletions packages/hydrogen/src/csp/Script.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {forwardRef} from 'react';
import {useNonce} from './csp';

type ScriptProps = JSX.IntrinsicElements['script'];

export const Script = forwardRef<HTMLScriptElement, ScriptProps>(
(props, ref) => {
const nonce = useNonce();
return <script {...props} nonce={nonce} ref={ref} />;
},
);
30 changes: 30 additions & 0 deletions packages/hydrogen/src/csp/csp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {createContext, useContext} from 'react';
import cspBuilder from 'content-security-policy-builder';

const CSPContext = createContext<string | undefined>(undefined);

export const CSPProvider = CSPContext.Provider;

export const useNonce = () => useContext(CSPContext);

export function createCSPHeader(nonce: string, directives = {}) {
const defaultDirectives = {
baseUri: ["'self'"],
defaultSrc: ["'self'", `'nonce-${nonce}'`, 'https://cdn.shopify.com'],
frameAncestors: ['none'],
styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.shopify.com'],
};
return cspBuilder({
directives: Object.assign({}, defaultDirectives, directives),
});
}

export function generateNonce() {
return toHexString(crypto.getRandomValues(new Uint8Array(16)));
}

function toHexString(byteArray: Uint8Array) {
return Array.from(byteArray, function (byte) {
return ('0' + (byte & 0xff).toString(16)).slice(-2);
}).join('');
}
3 changes: 3 additions & 0 deletions packages/hydrogen/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export type {
VariantOptionValue,
} from './product/VariantSelector';

export {createCSPHeader, CSPProvider, generateNonce, useNonce} from './csp/csp';
export {Script} from './csp/Script';

export {
AnalyticsEventName,
AnalyticsPageType,
Expand Down
11 changes: 6 additions & 5 deletions templates/skeleton/app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@ import type {EntryContext} from '@shopify/remix-oxygen';
import {RemixServer} from '@remix-run/react';
import isbot from 'isbot';
import {renderToReadableStream} from 'react-dom/server';
import {NonceProvider} from './utils';
import {generateNonce, CSPProvider, createCSPHeader} from '@shopify/hydrogen';

export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
const nonce = (request as any).nonce as string;

const nonce = generateNonce();
const body = await renderToReadableStream(
<NonceProvider value={nonce}>
<CSPProvider value={nonce}>
<RemixServer context={remixContext} url={request.url} />
</NonceProvider>,
</CSPProvider>,
{
nonce,
signal: request.signal,
Expand All @@ -32,6 +31,8 @@ export default async function handleRequest(
}

responseHeaders.set('Content-Type', 'text/html');
responseHeaders.set('Content-Security-Policy', createCSPHeader(nonce));

return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
Expand Down
2 changes: 1 addition & 1 deletion templates/skeleton/app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {useNonce} from '@shopify/hydrogen';
import {defer, type LoaderArgs} from '@shopify/remix-oxygen';
import {
Links,
Expand All @@ -17,7 +18,6 @@ import favicon from '../public/favicon.svg';
import resetStyles from './styles/reset.css';
import appStyles from './styles/app.css';
import {Layout} from '~/components/Layout';
import {useNonce} from './utils';

export function links() {
return [
Expand Down
4 changes: 0 additions & 4 deletions templates/skeleton/app/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,3 @@ export function getVariantUrl({

return path + (searchString ? '?' + searchParams.toString() : '');
}

export const NonceContext = createContext<string | undefined>(undefined);
export const NonceProvider = NonceContext.Provider;
export const useNonce = () => useContext(NonceContext);
14 changes: 0 additions & 14 deletions templates/skeleton/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ import {
type Session,
} from '@shopify/remix-oxygen';

function toHexString(byteArray: Uint8Array) {
return Array.from(byteArray, function (byte) {
return ('0' + (byte & 0xff).toString(16)).slice(-2);
}).join('');
}

/**
* Export a fetch handler in module format.
*/
Expand All @@ -44,9 +38,6 @@ export default {
HydrogenSession.init(request, [env.SESSION_SECRET]),
]);

const nonce = toHexString(crypto.getRandomValues(new Uint8Array(16)));
(request as any).nonce = nonce;

/**
* Create Hydrogen's Storefront client.
*/
Expand Down Expand Up @@ -84,11 +75,6 @@ export default {

const response = await handleRequest(request);

response.headers.set(
'Content-Security-Policy',
`base-uri 'self'; default-src 'self' 'nonce-${nonce}' https://cdn.shopify.com; frame-ancestors 'none'; style-src 'self' 'unsafe-inline' https://cdn.shopify.com;`,
);

if (response.status === 404) {
/**
* Check for redirects only when there's a 404 from the app.
Expand Down

0 comments on commit 8066aa5

Please sign in to comment.