@appsemble/node-utils 0.37.0 → 0.37.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +4 -4
- package/resource.d.ts +5 -0
- package/resource.js +1 -1
- package/server/routes/appRouter/indexHandler.d.ts +1 -1
- package/server/routes/appRouter/indexHandler.js +58 -11
- package/server/routes/appRouter/serviceWorkerHandler.js +12 -1
- package/server/types.d.ts +11 -1
- package/server/utils/actions.js +2 -21
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#  Appsemble Node Utilities
|
|
2
2
|
|
|
3
3
|
> NodeJS utilities used by Appsemble internally.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@appsemble/node-utils)
|
|
6
|
-
[](https://gitlab.com/appsemble/appsemble/-/releases/0.37.2)
|
|
7
7
|
[](https://prettier.io)
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
@@ -26,5 +26,5 @@ compatibility is not guaranteed.
|
|
|
26
26
|
|
|
27
27
|
## License
|
|
28
28
|
|
|
29
|
-
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.37.
|
|
29
|
+
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.37.2/LICENSE.md) ©
|
|
30
30
|
[Appsemble](https://appsemble.com)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/node-utils",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"description": "NodeJS utilities used by Appsemble internally.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"test": "vitest"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@appsemble/lang-sdk": "0.37.
|
|
44
|
-
"@appsemble/types": "0.37.
|
|
45
|
-
"@appsemble/utils": "0.37.
|
|
43
|
+
"@appsemble/lang-sdk": "0.37.2",
|
|
44
|
+
"@appsemble/types": "0.37.2",
|
|
45
|
+
"@appsemble/utils": "0.37.2",
|
|
46
46
|
"@formatjs/fast-memoize": "^2.0.0",
|
|
47
47
|
"@fortawesome/fontawesome-free": "^6.0.0",
|
|
48
48
|
"@inquirer/prompts": "^8.0.0",
|
package/resource.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export declare function serializeServerResource(data: any): JsonValue | {
|
|
|
17
17
|
assets: TempFile[];
|
|
18
18
|
};
|
|
19
19
|
export type SerializedServerResourceBody = ReturnType<typeof serializeServerResource>;
|
|
20
|
+
export type SerializedMultipartBody = Extract<SerializedServerResourceBody, {
|
|
21
|
+
resource: JsonValue;
|
|
22
|
+
assets: TempFile[];
|
|
23
|
+
}>;
|
|
24
|
+
export declare function isSerializedMultipartBody(value: SerializedServerResourceBody): value is SerializedMultipartBody;
|
|
20
25
|
/**
|
|
21
26
|
* Get the resource definition of an app by name.
|
|
22
27
|
*
|
package/resource.js
CHANGED
|
@@ -2,4 +2,4 @@ import { type Middleware } from 'koa';
|
|
|
2
2
|
import { type Options } from '../../types.js';
|
|
3
3
|
export declare const bulmaURL: string;
|
|
4
4
|
export declare const faURL: string;
|
|
5
|
-
export declare function createIndexHandler(
|
|
5
|
+
export declare function createIndexHandler(options: Options): Middleware;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { getAppBlocks } from '@appsemble/lang-sdk';
|
|
3
3
|
import { createThemeURL, defaultLocale, mergeThemes } from '@appsemble/utils';
|
|
4
4
|
import { organizationBlocklist } from '../../../organizationBlocklist.js';
|
|
@@ -6,8 +6,50 @@ import { makeCSP, render } from '../../../render.js';
|
|
|
6
6
|
import { bulmaVersion, faVersion } from '../../../versions.js';
|
|
7
7
|
export const bulmaURL = `/bulma/${bulmaVersion}/bulma.min.css`;
|
|
8
8
|
export const faURL = `/fa/${faVersion}/css/all.min.css`;
|
|
9
|
-
|
|
9
|
+
function hash(value) {
|
|
10
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('base64url');
|
|
11
|
+
}
|
|
12
|
+
function getAppUpdated(app) {
|
|
13
|
+
return app.$updated ?? '';
|
|
14
|
+
}
|
|
15
|
+
function getAppSnapshotId(app) {
|
|
16
|
+
return app.version ?? 'none';
|
|
17
|
+
}
|
|
18
|
+
function getSettingsCacheKey({ app, host, hostname, identifiableBlocks, languages, }) {
|
|
19
|
+
const blocks = identifiableBlocks
|
|
20
|
+
.map(({ type, version }) => ({ type, version }))
|
|
21
|
+
.sort((a, b) => a.type.localeCompare(b.type) || a.version.localeCompare(b.version));
|
|
22
|
+
return [
|
|
23
|
+
'app-settings',
|
|
24
|
+
app.id,
|
|
25
|
+
getAppUpdated(app),
|
|
26
|
+
getAppSnapshotId(app),
|
|
27
|
+
hash({ host, hostname }),
|
|
28
|
+
hash(languages),
|
|
29
|
+
hash(blocks),
|
|
30
|
+
].join(':');
|
|
31
|
+
}
|
|
32
|
+
function getMessagesCacheKey({ app }) {
|
|
33
|
+
return ['app-messages', app.id, getAppUpdated(app)].join(':');
|
|
34
|
+
}
|
|
35
|
+
async function getCachedValue(cache, key, fallback) {
|
|
36
|
+
if (!cache) {
|
|
37
|
+
return [await fallback(), 'disabled'];
|
|
38
|
+
}
|
|
39
|
+
const cached = await cache.get(key);
|
|
40
|
+
if (cached.status === 'hit') {
|
|
41
|
+
return [cached.value, 'hit'];
|
|
42
|
+
}
|
|
43
|
+
const value = await fallback();
|
|
44
|
+
if (cached.status === 'miss') {
|
|
45
|
+
const setStatus = await cache.set(key, value);
|
|
46
|
+
return [value, setStatus === 'error' ? 'error' : 'miss'];
|
|
47
|
+
}
|
|
48
|
+
return [value, cached.status];
|
|
49
|
+
}
|
|
50
|
+
export function createIndexHandler(options) {
|
|
10
51
|
return async (ctx) => {
|
|
52
|
+
const { appServingCache, createSettings, getApp, getAppDetails, getAppMessages, getAppUrl, getCsp, getHost, } = options;
|
|
11
53
|
const { hostname, path } = ctx;
|
|
12
54
|
const host = getHost({ context: ctx });
|
|
13
55
|
// Prevent mime-type sniffing,
|
|
@@ -40,21 +82,26 @@ export function createIndexHandler({ createSettings, getApp, getAppDetails, getA
|
|
|
40
82
|
return;
|
|
41
83
|
}
|
|
42
84
|
const defaultLanguage = app.definition.defaultLanguage || defaultLocale;
|
|
43
|
-
const appMessages = await getAppMessages({ app, context: ctx });
|
|
85
|
+
const [appMessages, messagesCacheStatus] = await getCachedValue(appServingCache, getMessagesCacheKey({ app }), () => getAppMessages({ app, context: ctx }));
|
|
86
|
+
ctx.set('X-Appsemble-Messages-Cache', messagesCacheStatus);
|
|
44
87
|
const languages = [
|
|
45
88
|
...new Set([...appMessages.map(({ language }) => language), defaultLanguage]),
|
|
46
89
|
].sort();
|
|
47
90
|
const identifiableBlocks = getAppBlocks(app.definition);
|
|
48
91
|
const nonce = randomBytes(16).toString('base64');
|
|
49
|
-
const [settingsHash, settings] = await
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
92
|
+
const [{ settingsHash, settings }, settingsCacheStatus] = await getCachedValue(appServingCache, getSettingsCacheKey({ app, host, hostname, identifiableBlocks, languages }), async () => {
|
|
93
|
+
const [digest, script] = await createSettings({
|
|
94
|
+
context: ctx,
|
|
95
|
+
app,
|
|
96
|
+
host,
|
|
97
|
+
hostname,
|
|
98
|
+
identifiableBlocks,
|
|
99
|
+
languages,
|
|
100
|
+
nonce,
|
|
101
|
+
});
|
|
102
|
+
return { settingsHash: digest, settings: script };
|
|
57
103
|
});
|
|
104
|
+
ctx.set('X-Appsemble-Settings-Cache', settingsCacheStatus);
|
|
58
105
|
const csp = getCsp({ app, settingsHash, hostname, host, nonce });
|
|
59
106
|
ctx.set('Content-Security-Policy', makeCSP(csp));
|
|
60
107
|
const updated = app.$updated ? new Date(app.$updated) : new Date();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { readFile } from 'node:fs/promises';
|
|
2
3
|
export function createServiceWorkerHandler() {
|
|
3
4
|
return async (ctx) => {
|
|
@@ -6,8 +7,18 @@ export function createServiceWorkerHandler() {
|
|
|
6
7
|
const serviceWorker = await (production
|
|
7
8
|
? readFile(new URL('../../../../../dist/app/service-worker.js', import.meta.url), 'utf8')
|
|
8
9
|
: ctx.fs.promises.readFile(filename, 'utf8'));
|
|
9
|
-
ctx.body = serviceWorker;
|
|
10
10
|
ctx.type = 'application/javascript';
|
|
11
|
+
// Browsers bypass their HTTP cache when checking for service worker updates, so a strong ETag
|
|
12
|
+
// lets them revalidate with a 304 instead of downloading the full script on every check.
|
|
13
|
+
ctx.set('etag', `"${createHash('sha256').update(serviceWorker).digest('base64url')}"`);
|
|
14
|
+
ctx.set('cache-control', 'no-cache');
|
|
15
|
+
ctx.status = 200;
|
|
16
|
+
if (ctx.fresh) {
|
|
17
|
+
ctx.status = 304;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
ctx.body = serviceWorker;
|
|
21
|
+
}
|
|
11
22
|
};
|
|
12
23
|
}
|
|
13
24
|
//# sourceMappingURL=serviceWorkerHandler.js.map
|
package/server/types.d.ts
CHANGED
|
@@ -404,6 +404,15 @@ export interface ParsedQuery {
|
|
|
404
404
|
where: WhereOptions;
|
|
405
405
|
}
|
|
406
406
|
export type ContentSecurityPolicy = Record<string, (string | false)[]>;
|
|
407
|
+
export type AppServingCacheStatus = 'disabled' | 'error' | 'hit' | 'miss';
|
|
408
|
+
export interface AppServingCacheResult<T> {
|
|
409
|
+
status: AppServingCacheStatus;
|
|
410
|
+
value?: T;
|
|
411
|
+
}
|
|
412
|
+
export interface AppServingCache {
|
|
413
|
+
get: <T>(key: string) => Promise<AppServingCacheResult<T>>;
|
|
414
|
+
set: <T>(key: string, value: T) => Promise<AppServingCacheStatus>;
|
|
415
|
+
}
|
|
407
416
|
export interface Options {
|
|
408
417
|
getSecurityEmail: () => string;
|
|
409
418
|
getCurrentAppMember: (params: GetCurrentAppMemberParams) => Promise<AppMemberInfo | null>;
|
|
@@ -423,11 +432,12 @@ export interface Options {
|
|
|
423
432
|
getBlockMessages: (params: GetBlockMessagesParams) => Promise<BlockMessages[]>;
|
|
424
433
|
getBlockAsset: (params: GetBlockAssetParams) => Promise<ProjectAsset>;
|
|
425
434
|
getBlocksAssetsPaths: (params: GetBlocksAssetsPathsParams) => Promise<string[]>;
|
|
426
|
-
getTheme: (params: GetThemeParams) => Promise<Theme>;
|
|
435
|
+
getTheme: (params: GetThemeParams) => Promise<Theme | null>;
|
|
427
436
|
createTheme: (params: CreateThemeParams) => Promise<Theme>;
|
|
428
437
|
getHost: (params: GetHostParams) => string;
|
|
429
438
|
getCsp: (params: GetCspParams) => ContentSecurityPolicy;
|
|
430
439
|
createSettings: (params: CreateSettingsParams) => Promise<[digest: string, script: string]>;
|
|
440
|
+
appServingCache?: AppServingCache;
|
|
431
441
|
applyAppServiceSecrets: (params: ApplyAppServiceSecretsParams) => Promise<RawAxiosRequestConfig<any>>;
|
|
432
442
|
checkAppMemberAppPermissions: (params: CheckAppMemberAppPermissionsParams) => Promise<void>;
|
|
433
443
|
checkUserOrganizationPermissions: (params: CheckUserOrganizationPermissionsParams) => Promise<void>;
|
package/server/utils/actions.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { defaultLocale, remap, } from '@appsemble/lang-sdk';
|
|
2
2
|
import { assertKoaCondition, createFormData, EmailQuotaExceededError, getContainerNamespace, getRemapperContext, getSSRFProtectedAgents, logger, parseServiceUrl, scaleDeployment, setLastRequestAnnotation, throwKoaError, version, waitForPodReadiness, } from '@appsemble/node-utils';
|
|
3
|
+
import { deserializeResource } from '@appsemble/utils';
|
|
3
4
|
import axios from 'axios';
|
|
4
|
-
import { get,
|
|
5
|
+
import { get, pick } from 'lodash-es';
|
|
5
6
|
/**
|
|
6
7
|
* These response headers are forwarded when proxying requests.
|
|
7
8
|
*/
|
|
@@ -40,26 +41,6 @@ export async function handleNotify(ctx, app, action, options) {
|
|
|
40
41
|
await sendNotifications({ app, to, title, body, link });
|
|
41
42
|
ctx.status = 204;
|
|
42
43
|
}
|
|
43
|
-
function deserializeResource(data) {
|
|
44
|
-
// Extract the resource and assets from the JSON object
|
|
45
|
-
const { resource } = data;
|
|
46
|
-
const assets = data.assets;
|
|
47
|
-
// Function to replace asset placeholders with actual Blobs
|
|
48
|
-
const replaceAssets = (value) => {
|
|
49
|
-
if (Array.isArray(value)) {
|
|
50
|
-
return value.map(replaceAssets);
|
|
51
|
-
}
|
|
52
|
-
if (typeof value === 'string' && /^\d+$/.test(value)) {
|
|
53
|
-
return assets[Number(value)];
|
|
54
|
-
}
|
|
55
|
-
if (value && typeof value === 'object') {
|
|
56
|
-
return mapValues(value, replaceAssets);
|
|
57
|
-
}
|
|
58
|
-
return value;
|
|
59
|
-
};
|
|
60
|
-
// Replace placeholders and return the deserialized resource
|
|
61
|
-
return replaceAssets(resource);
|
|
62
|
-
}
|
|
63
44
|
async function handleRequestProxy(ctx, app, action, useBody, options) {
|
|
64
45
|
const { method, query, request: { body, headers }, } = ctx;
|
|
65
46
|
let data;
|