diff --git a/sandpack-client/src/clients/runtime/index.ts b/sandpack-client/src/clients/runtime/index.ts index cd8973a94..5b7c2e818 100644 --- a/sandpack-client/src/clients/runtime/index.ts +++ b/sandpack-client/src/clients/runtime/index.ts @@ -58,6 +58,12 @@ export class SandpackRuntime extends SandpackClient { this.bundlerURL = options.bundlerURL || BUNDLER_URL; + if (options.teamId) { + this.bundlerURL = + this.bundlerURL.replace("https://", "https://" + options.teamId + "-") + + `?cache=${Date.now()}`; + } + this.bundlerState = undefined; this.errors = []; this.status = "initializing"; @@ -241,6 +247,7 @@ export class SandpackRuntime extends SandpackClient { clearConsoleDisabled: !this.options.clearConsoleOnFirstCompile, logLevel: this.options.logLevel ?? SandpackLogLevel.Info, customNpmRegistries: this.options.customNpmRegistries, + teamId: this.options.teamId, }); } diff --git a/sandpack-client/src/clients/runtime/types.ts b/sandpack-client/src/clients/runtime/types.ts index 5d99409ce..670724ad7 100644 --- a/sandpack-client/src/clients/runtime/types.ts +++ b/sandpack-client/src/clients/runtime/types.ts @@ -78,6 +78,7 @@ export type SandpackRuntimeMessage = BaseSandpackMessage & reactDevTools?: ReactDevToolsMode; logLevel?: SandpackLogLevel; customNpmRegistries?: NpmRegistry[]; + teamId?: string; } | { type: "refresh"; @@ -103,4 +104,6 @@ export type SandpackRuntimeMessage = BaseSandpackMessage & }>; } | SandboxTestMessage + | { type: "sign-in"; teamId: string } + | { type: "sign-out" } ); diff --git a/sandpack-client/src/types.ts b/sandpack-client/src/types.ts index 757cd77f3..3e11816ec 100644 --- a/sandpack-client/src/types.ts +++ b/sandpack-client/src/types.ts @@ -59,6 +59,12 @@ export interface ClientOptions { * to retrieve npm packages from your own npm registry. */ customNpmRegistries?: NpmRegistry[]; + + /** + * CodeSandbox team id: with this information, bundler can connect to CodeSandbox + * and unlock a few capabilities + */ + teamId?: string; } export interface SandboxSetup { diff --git a/sandpack-client/src/utils.ts b/sandpack-client/src/utils.ts index 5cb865d87..93a7de10c 100644 --- a/sandpack-client/src/utils.ts +++ b/sandpack-client/src/utils.ts @@ -104,7 +104,7 @@ export function extractErrorDetails(msg: SandpackErrorMessage): SandpackError { return { title, path, message, line, column }; } - const relevantStackFrame = getRelevantStackFrame(msg.payload.frames); + const relevantStackFrame = getRelevantStackFrame(msg.payload?.frames); if (!relevantStackFrame) { return { message: msg.message }; } diff --git a/sandpack-react/src/PrivatePackage.stories.tsx b/sandpack-react/src/PrivatePackage.stories.tsx new file mode 100644 index 000000000..be40526ec --- /dev/null +++ b/sandpack-react/src/PrivatePackage.stories.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +import { Sandpack } from "./presets"; + +export default { + title: "Intro/PrivatePackage", +}; + +export const Basic: React.FC = () => { + return ( +
+ Hello World + // }`, + // }} + // options={{ bundlerURL: `http://localhost:3000` }} + options={{ bundlerURL: `https://2-1-0-sandpack.codesandbox.stream/` }} + teamId="6756547b-12fb-465e-82c8-b38a981f1f67" + template="react" + /> +
+ ); +}; diff --git a/sandpack-react/src/components/Preview/index.tsx b/sandpack-react/src/components/Preview/index.tsx index 44bb282fe..b24a3e800 100644 --- a/sandpack-react/src/components/Preview/index.tsx +++ b/sandpack-react/src/components/Preview/index.tsx @@ -10,6 +10,11 @@ import { useSandpackShell, } from "../../hooks"; import { css, THEME_PREFIX } from "../../styles"; +import { + buttonClassName, + iconStandaloneClassName, + roundedButtonClassName, +} from "../../styles/shared"; import { useClassNames } from "../../utils/classNames"; import { Navigator } from "../Navigator"; import { ErrorOverlay } from "../common/ErrorOverlay"; @@ -18,6 +23,7 @@ import { OpenInCodeSandboxButton } from "../common/OpenInCodeSandboxButton"; import { RoundedButton } from "../common/RoundedButton"; import { SandpackStack } from "../common/Stack"; import { RefreshIcon, RestartIcon } from "../icons"; +import { SignOutIcon } from "../icons"; export interface PreviewProps { style?: React.CSSProperties; @@ -109,9 +115,8 @@ export const SandpackPreview = React.forwardRef< }, ref ) => { - const { sandpack, listen, iframe, getClient, clientId } = useSandpackClient( - { startRoute } - ); + const { sandpack, listen, iframe, getClient, clientId, dispatch } = + useSandpackClient({ startRoute }); const [iframeComputedHeight, setComputedAutoHeight] = React.useState< number | null >(null); @@ -193,6 +198,22 @@ export const SandpackPreview = React.forwardRef< )} + {sandpack.teamId && ( + + )} + {showOpenInCodeSandbox && } diff --git a/sandpack-react/src/components/common/ErrorOverlay.tsx b/sandpack-react/src/components/common/ErrorOverlay.tsx index 2e8cefdf5..228716610 100644 --- a/sandpack-react/src/components/common/ErrorOverlay.tsx +++ b/sandpack-react/src/components/common/ErrorOverlay.tsx @@ -12,6 +12,7 @@ import { roundedButtonClassName, } from "../../styles/shared"; import { useClassNames } from "../../utils/classNames"; +import { SignInIcon } from "../icons"; import { RestartIcon } from "../icons"; const mapBundlerErrors = (originalMessage: string): string => { @@ -42,14 +43,68 @@ export const ErrorOverlay: React.FC = (props) => { const { restart } = useSandpackShell(); const classNames = useClassNames(); const { - sandpack: { runSandpack }, + sandpack: { runSandpack, teamId }, } = useSandpack(); + const { dispatch } = useSandpack(); if (!errorMessage && !children) { return null; } const isSandpackBundlerError = errorMessage?.startsWith("[sandpack-client]"); + const privateDependencyError = errorMessage?.includes( + "NPM_REGISTRY_UNAUTHENTICATED_REQUEST" + ); + + const onSignIn = () => { + if (teamId) { + dispatch({ type: "sign-in", teamId }); + } + }; + + if (privateDependencyError) { + return ( +
+

+ Unable to fetch required dependency. +

+ +
+

+ Authentication required. Please sign in to your account (make sure + to allow pop-ups to this page) and try again. If the issue persists, + contact{" "} + + support + {" "} + for further assistance. +

+
+ +
+ +
+
+ ); + } if (isSandpackBundlerError && errorMessage) { return ( @@ -98,15 +153,23 @@ export const ErrorOverlay: React.FC = (props) => { className={classNames("overlay", [ classNames("error"), absoluteClassName, - errorClassName, + errorClassName({ solidBg: true }), className, ])} translate="no" {...otherProps} > -
+

+ Something went wrong +

+ +

{errorMessage || children} -

+

); }; diff --git a/sandpack-react/src/components/common/LoadingOverlay.tsx b/sandpack-react/src/components/common/LoadingOverlay.tsx index 58f143d93..bddefbd2b 100644 --- a/sandpack-react/src/components/common/LoadingOverlay.tsx +++ b/sandpack-react/src/components/common/LoadingOverlay.tsx @@ -12,6 +12,7 @@ import { absoluteClassName, buttonClassName, errorBundlerClassName, + errorClassName, errorMessageClassName, fadeIn, iconStandaloneClassName, @@ -39,15 +40,16 @@ const loadingClassName = css({ backgroundColor: "$colors$surface1", }); -export const LoadingOverlay = ({ +export const LoadingOverlay: React.FC< + LoadingOverlayProps & React.HTMLAttributes +> = ({ clientId, loading, className, style, showOpenInCodeSandbox, ...props -}: LoadingOverlayProps & - React.HTMLAttributes): JSX.Element | null => { +}): JSX.Element | null => { const classNames = useClassNames(); const { sandpack: { runSandpack, environment }, @@ -83,6 +85,7 @@ export const LoadingOverlay = ({ className={classNames("overlay", [ classNames("error"), absoluteClassName, + errorClassName, errorBundlerClassName, className, ])} @@ -95,28 +98,34 @@ export const LoadingOverlay = ({ Couldn't connect to server

-

- This means sandpack cannot connect to the runtime or your network is - having some issues. Please check the network tab in your browser and - try again. If the problem persists, report it via{" "} - - email - {" "} - or submit an issue on{" "} - - GitHub. - -

+
+

+ This means sandpack cannot connect to the runtime or your network + is having some issues. Please check the network tab in your + browser and try again. If the problem persists, report it via{" "} + + email + {" "} + or submit an issue on{" "} + + GitHub. + +

+
-
+          

ENV: {environment}
ERROR: TIME_OUT -

+

+}`, + }} +/> + +``` + +4. **Sign-in:** When Sandpack loads, you will be asked to sign in; after doing that, it's done. + + + + + + +## Security + +It is important to us to ensure that the information and tokens of the npm registry are kept private. As such, we have added some extra measures to prevent any type of leakage. + +### Persisting the `auth` token + +The `auth` token is stored in our database in an encrypted form, using a key that's rolled and not available to the database itself. Even in the unlikely event that our database gets compromised, your `auth` token would be encrypted and inaccessible. + +### Single-Sandbox key + +We never send the `auth` token to the browser. Instead, we give every editor of the sandbox a key that only gives them access to that specific sandbox. + +If they want to retrieve a package from the private npm registry, they will have to ask our API. The API will fetch the `auth` token, request the npm registry on behalf of the user and return the response to the user. + +### Trusted domain only + +Sandpack uses the HTTP Content-Security-Policy (CSP) frame-ancestors directive to validate parents' window that may embed the given sandpack iFrame. + +With this extra security layer, only websites you trust (and provide to us) can render and fetch private packages from your registry. This can avoid phishing websites and the potential leakage of private data. + diff --git a/website/docs/src/pages/guides/private-packages.mdx b/website/docs/src/pages/guides/private-packages.mdx deleted file mode 100644 index ca39d972c..000000000 --- a/website/docs/src/pages/guides/private-packages.mdx +++ /dev/null @@ -1,66 +0,0 @@ -# Private Packages - -
- - - The custom private NPM registry allows Sandpack instances to retrieve private - NPM packages from your own registry. This option requires running a third - service (Node.js server) and configuring your Sandpack provider to consume - these dependencies from another registry, not the public ones. - - -![Private packages](/private-package.png) - -**You'll need:** - -- Host a Node.js server, which will run registry proxy; -- GitHub/NPM authentication token with read access; - -## Self-host the proxy - -We recommend hosting a service that allows you to proxy your private packages from a registry (GitHub/Npm/your own) to a new one, which would make the packages available through another URL. -As Sandpack bundles everything in-browser, it needs to find a way to connect to the registry which provides the project dependencies. -First, Sandpack will try to fetch all dependencies from public registries, for example, `react` or `redux`. Then you can let Sandpack know which dependencies (or scoped dependencies) should be fetched from a different registry. For example, your custom registry. - -### Our recommendation - -Suppose you don't already have a public registry, we recommend using [Verdaccio](https://verdaccio.org/). An open-source project that creates a private registry and can proxy other registries, such as GitHub and Npm. -You can find examples of how to use the [examples folder](https://github.com/codesandbox/sandpack/tree/main/examples) in the main repository. - -## Sandpack configuration - -Once the proxy is running and configured, you need to set some options in your Sandpack context: - -```jsx - - -
- ) -} -`, - }} - template="react" -/> -``` - -## Security - -It's essential to keep the information and tokens of the npm registry private! By using this method, it's best to keep in mind that it could expose all private packages in your account. Be careful where and how this proxy will be used. Make sure to use authentication tokens with **read-only access**. - -It's also possible to expose only specific packages. If the custom scopes are `@scope/package-name` instead of `@scope/*`, it will only expose that particular package. You can even do something like `@scope/design-system*` to expose all packages of the design system.