Skip to content

App scheme ​

An app that serves its built page from a scheme of its own, such as app://app, gets a real origin without a server. createAppScheme serves the page and the other hosts of that scheme from their folders, and binds the scheme and the origin to the web guards, so a call site names neither again.

The package registers nothing itself. The app hands privileged to Electron before it is ready, and handle to the sessions it picks:

ts
import { app, protocol, session } from 'electron';
import { createAppScheme } from '@adecore/shell';

const scheme = createAppScheme({
    scheme: 'app',
    root: join(process.resourcesPath, 'client'),
    headers: { 'content-security-policy': CSP },
    pageUrl: process.env.DEV_SERVER_URL
});

protocol.registerSchemesAsPrivileged([scheme.privileged]);

app.whenReady().then(() => {
    session.defaultSession.protocol.handle(scheme.scheme, scheme.handle);
    window.loadURL(scheme.url);
});

Options ​

AppSchemeOptions:

OptionDefaultMeaning
schemerequiredThe scheme without its colon
hostappThe host of the page
rootrequiredThe folder of the built page
routestrueThe page routes itself: an address without an extension gets its index.html
headersnoneHeaders on every response of the page, such as its policy, or a function of the file
hostsnoneOther hosts on the same scheme, by name: a SchemeFolder or a SchemeResolver
pageUrlthe page's hostThe URL the window loads instead, such as a dev server; the guards take its origin
privilegesAPP_SCHEME_PRIVILEGESWhat Chromium allows the scheme
readreads the file wholeAnswers with a file it found; (file) => net.fetch(pathToFileURL(file).href) streams it

APP_SCHEME_PRIVILEGES is standard and secure for a real origin and a secure context, fetch and CORS, streaming, the code cache and service workers.

What it serves ​

A request is answered from the folder of its host. / is the folder's index.html, and so is an address without an extension when the folder routes itself. A file it lacks is a 404, also a chunk that a newer build replaced, so a stale one fails as a load error instead of loading the page. A segment that could climb out (.., an escaped slash) and a symlink that leads outside the folder are a 404 as well. The content type comes from the extension, application/wasm included, which a WebAssembly module needs to compile while it streams.

A SchemeFolder is { root, routes?, headers? }. A host whose files are not one folder, such as files found by an id, is a SchemeResolver: (request, url) => Response. It finds the file itself and answers through serveFile(root, file, request, headers?), which serves it only when it lies inside root. segmentsOf(pathname) gives the decoded segments of a path, or null when one could climb out.

ts
const scheme = createAppScheme({
    scheme: 'app',
    root: clientRoot,
    hosts: {
        render: { root: renderRoot, headers: { 'access-control-allow-origin': '*' } },
        attachment: async (request, url) => {
            const [folder, id] = segmentsOf(url.pathname) ?? [];
            const file = folder && id ? await findAttachment(folder, id) : null;
            return file === null ? new Response('Not found', { status: 404 }) : scheme.serveFile(attachmentsRoot, file, request);
        }
    }
});

The guards ​

The AppScheme it returns holds the guards. origin is what the guards take for the app's, and url is what the window loads. The guards are bound to both: originOf(url), isAppUrl(url), navigation(url) for appWindowNavigation and isAppSender(isAppWindow, frame). With pageUrl the app's origin is the dev server's, so the page's own host on the scheme is then not the app.

ts
const fromApp = (event: Electron.IpcMainInvokeEvent): boolean => scheme.isAppSender(isAppWindow(event.sender), event.senderFrame);

Licenses and third-party notices are listed in each package.