@appsemble/node-utils
Version:
NodeJS utilities used by Appsemble internally.
126 lines • 5.69 kB
JavaScript
import { createHash, randomBytes } from 'node:crypto';
import { getAppBlocks } from '@appsemble/lang-sdk';
import { createThemeURL, defaultLocale, mergeThemes } from '@appsemble/utils';
import { organizationBlocklist } from '../../../organizationBlocklist.js';
import { makeCSP, render } from '../../../render.js';
import { bulmaVersion, faVersion } from '../../../versions.js';
export const bulmaURL = `/bulma/${bulmaVersion}/bulma.min.css`;
export const faURL = `/fa/${faVersion}/css/all.min.css`;
function hash(value) {
return createHash('sha256').update(JSON.stringify(value)).digest('base64url');
}
function getAppUpdated(app) {
return app.$updated ?? '';
}
function getAppSnapshotId(app) {
return app.version ?? 'none';
}
function getSettingsCacheKey({ app, host, hostname, identifiableBlocks, languages, }) {
const blocks = identifiableBlocks
.map(({ type, version }) => ({ type, version }))
.sort((a, b) => a.type.localeCompare(b.type) || a.version.localeCompare(b.version));
return [
'app-settings',
app.id,
getAppUpdated(app),
getAppSnapshotId(app),
hash({ host, hostname }),
hash(languages),
hash(blocks),
].join(':');
}
function getMessagesCacheKey({ app }) {
return ['app-messages', app.id, getAppUpdated(app)].join(':');
}
async function getCachedValue(cache, key, fallback) {
if (!cache) {
return [await fallback(), 'disabled'];
}
const cached = await cache.get(key);
if (cached.status === 'hit') {
return [cached.value, 'hit'];
}
const value = await fallback();
if (cached.status === 'miss') {
const setStatus = await cache.set(key, value);
return [value, setStatus === 'error' ? 'error' : 'miss'];
}
return [value, cached.status];
}
export function createIndexHandler(options) {
return async (ctx) => {
const { appServingCache, createSettings, getApp, getAppDetails, getAppMessages, getAppUrl, getCsp, getHost, } = options;
const { hostname, path } = ctx;
const host = getHost({ context: ctx });
// Prevent mime-type sniffing,
// Visit https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options to know more.
ctx.set('x-content-type-options', 'nosniff');
// Less strict due to the OAuth mechanism
// Visit https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy to know more.
ctx.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Most of the fields here are used either directly or indirectly that's why no attributes query
const app = await getApp({ context: ctx });
if (!app) {
const { appPath, organizationId } = await getAppDetails({ context: ctx });
if (organizationId && !appPath) {
return ctx.redirect(organizationBlocklist.includes(organizationId)
? host
: String(new URL(`/organizations/${organizationId}`, host)));
}
ctx.status = 404;
return render(ctx, 'app/error.html', {
bulmaURL,
faURL,
message: 'The app you are looking for could not be found.',
});
}
const appUrl = await getAppUrl({ app, context: ctx });
if (appUrl.hostname !== hostname) {
const redirectUrl = new URL(path, appUrl);
redirectUrl.search = ctx.search;
ctx.redirect(String(redirectUrl));
return;
}
const defaultLanguage = app.definition.defaultLanguage || defaultLocale;
const [appMessages, messagesCacheStatus] = await getCachedValue(appServingCache, getMessagesCacheKey({ app }), () => getAppMessages({ app, context: ctx }));
ctx.set('X-Appsemble-Messages-Cache', messagesCacheStatus);
const languages = [
...new Set([...appMessages.map(({ language }) => language), defaultLanguage]),
].sort();
const identifiableBlocks = getAppBlocks(app.definition);
const nonce = randomBytes(16).toString('base64');
const [{ settingsHash, settings }, settingsCacheStatus] = await getCachedValue(appServingCache, getSettingsCacheKey({ app, host, hostname, identifiableBlocks, languages }), async () => {
const [digest, script] = await createSettings({
context: ctx,
app,
host,
hostname,
identifiableBlocks,
languages,
nonce,
});
return { settingsHash: digest, settings: script };
});
ctx.set('X-Appsemble-Settings-Cache', settingsCacheStatus);
const csp = getCsp({ app, settingsHash, hostname, host, nonce });
ctx.set('Content-Security-Policy', makeCSP(csp));
const updated = app.$updated ? new Date(app.$updated) : new Date();
return render(ctx, 'app/index.html', {
app,
noIndex: app.visibility !== 'public',
appUrl: String(appUrl),
host,
locale: defaultLanguage,
locales: languages.filter((lang) => lang !== defaultLanguage),
// @ts-expect-error 2345 argument of type is not assignable to parameter of type
// (strictNullChecks)
bulmaURL: createThemeURL(mergeThemes(app.definition.theme)),
faURL,
nonce,
settings,
themeColor: app.definition.theme?.themeColor || '#ffffff',
appUpdated: updated.toISOString(),
});
};
}
//# sourceMappingURL=indexHandler.js.map