@meddleware/frontend-config 0.0.2 → 0.0.4
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/CHANGELOG.md +18 -0
- package/README.md +9 -2
- package/dist/audit-gate.d.ts +2 -0
- package/dist/check-csp-inline.d.ts +2 -0
- package/dist/csp.d.ts +46 -0
- package/dist/network-status.d.ts +8 -0
- package/dist/sync.d.ts +13 -0
- package/dist/third-party-licenses.d.ts +2 -0
- package/dist/vite.d.ts +25 -0
- package/dist/vitest.d.ts +20 -0
- package/package.json +16 -4
- package/src/audit-gate.js +67 -0
- package/src/check-csp-inline.js +68 -0
- package/src/csp.js +67 -0
- package/src/network-status.js +45 -0
- package/src/sync.js +80 -0
- package/src/third-party-licenses.js +77 -0
- package/src/vite.js +31 -0
- package/src/vitest.js +29 -0
- package/templates/audit-allowlist.lhci.json +12 -0
- package/templates/dependabot.app.yml +32 -0
- package/templates/dockerignore +13 -0
- package/templates/postcss.config.js +1 -0
- package/templates/stylelint.config.js +3 -0
- package/tsconfig/vue-app.json +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.0.4] - 2026-11-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `mw-network-status`: writes the networks the platform is deployed on from the clients' deployment records, for the landing page, docs and developer docs.
|
|
13
|
+
|
|
14
|
+
## [0.0.3] - 2026-11-07
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `mw-audit-gate`, `mw-third-party-licenses` and `mw-check-csp-inline`: the dependency-audit gate, the licence bundle and the inline-script
|
|
19
|
+
CSP check that every app repository carried as its own copy of a script.
|
|
20
|
+
- `/csp`: `buildCsp`, `cspForProfile` and six named profiles that reproduce, byte for byte, the policies the app images served.
|
|
21
|
+
- `meddlewareApp` (`/vite`), `vueTestConfig` (`/vitest`) and `tsconfig/vue-app.json`: the Vite, Vitest and TypeScript configuration that eleven
|
|
22
|
+
apps each repeated.
|
|
23
|
+
- `mw-sync --check|--write` and `templates/`: the files that cannot be shared by reference stay identical across repositories, and the
|
|
24
|
+
Dockerfile's `ARG CSP` is held equal to the repository's profile.
|
|
25
|
+
|
|
8
26
|
## [0.0.2] - 2026-11-01
|
|
9
27
|
|
|
10
28
|
### Added
|
package/README.md
CHANGED
|
@@ -10,16 +10,23 @@ same accessibility and browser-support bar.
|
|
|
10
10
|
| `/stylelint` | `stylelintConfig({ tokens?, compat?, browsers?, rules? })` — standard config, no hex, token-only colour and `z-index`, `@double-great/stylelint-a11y`, `stylelint-no-unsupported-browser-features`. |
|
|
11
11
|
| `/html-validate` | `htmlValidateConfig({ elements?, rules? })` — recommended + `html-validate:a11y` + Vue, with `@meddleware/ui` components described as the native element they render. |
|
|
12
12
|
| `/postcss` | Autoprefixer for the repo's browserslist. |
|
|
13
|
-
| `/vite` | `viteTarget()` — Vite `build.target` derived from the browserslist; `themeInit({ default })` — a Vite plugin that loads ui's `theme-init.js` first in `<head>` (the saved or default colour mode and the season applied before first paint, no inline script, no CSP hash) and adds `color-scheme` and the browser-chrome `theme-color` from the tokens. |
|
|
13
|
+
| `/vite` | `viteTarget()` — Vite `build.target` derived from the browserslist; `meddlewareApp({ plugins, theme?, wallet?, config? })` — the whole Vite configuration of a tool app (target, theme-init, e2e guard, wallet-adapter kept out of pre-bundling); `themeInit({ default })` — a Vite plugin that loads ui's `theme-init.js` first in `<head>` (the saved or default colour mode and the season applied before first paint, no inline script, no CSP hash) and adds `color-scheme` and the browser-chrome `theme-color` from the tokens. |
|
|
14
14
|
| `/playwright` | `definePlaywrightConfig({ port, kind?, command?, browsers?, extra? })`. |
|
|
15
15
|
| `/axe` | `expectAccessible(page, options)` — WCAG 2.2 AA gate; AAA as `aaa: 'gate'`, `'report'` or `'off'`. |
|
|
16
16
|
| `/look` | `looks()`, `applyLook(page, theme, season)`, `offline(page)`, `VIEWPORTS`, `THEMES`, `SEASONS` — the viewport × colour mode × season matrix. |
|
|
17
17
|
| `/shots` | `defineShots(states)` — one screenshot per state × look, for looking at the UI. |
|
|
18
18
|
| `/a11y` | `defineA11ySuite(states, { aaa })` — the sweep every app runs, one call: each state in every colour mode × season at WCAG 2.2 AA and AAA, a 320 px reflow, 200% text size, forced colours, reduced motion and a keyboard pass. `expectNoHorizontalScroll`, `expectNoRunningAnimations` and `expectKeyboardFocusVisible` are exported on their own. |
|
|
19
|
-
| `/vitest` | `
|
|
19
|
+
| `/vitest` | `vueTestConfig({ plugins, test? })` — the whole Vitest configuration of a Vue repository (jsdom, `tests/**/*.test.ts`, one copy of Vue, the UI packages processed through Vite); `inlineMeddleware` — those packages on their own; `installDialogShim()` — jsdom's missing `<dialog>` modality. |
|
|
20
20
|
| `/bundle` + `check-prod-bundle` | `scanBundle(dist, { also })` and the `check-prod-bundle [dist] [--also TEXT]... [--also-regex PATTERN]...` command: fails when the e2e harness (the mock-wallet hooks, or an app's own stand-in marker) is found in a production build. Run it after the production build in CI and in the Dockerfile. |
|
|
21
21
|
| `/wallet` | `connectMockWallet(page)`, `exposeSigner(page, signer)`, `E2E_ADDRESS` — the Playwright side of the mock wallet. |
|
|
22
22
|
| `/lighthouserc` | `lighthouseConfig({ port, paths?, … })` — Lighthouse CI (accessibility must score 100). |
|
|
23
|
+
| `/tsconfig/vue-app.json` | The compiler options of a Vue app (`"extends": "@meddleware/frontend-config/tsconfig/vue-app.json"`); the repository keeps only its `include`. |
|
|
24
|
+
| `/csp` | `buildCsp(options)`, `cspForProfile(name, current?)`, `PROFILES`, `scriptHashesOf(csp)` — the Content-Security-Policy every app image serves, as six named profiles (`static`, `static-strict`, `console`, `chain`, `chain-wasm`, `docs`), so the policy is written once. |
|
|
25
|
+
| `mw-sync` | `mw-sync --check` / `--write` in a repository root: the files every frontend repository copies (dependabot.yml, .dockerignore, postcss/stylelint configs, the Lighthouse allowlist) equal this package's `templates/`, and the Dockerfile's `ARG CSP` equals its profile. Declared in package.json: `"meddleware": { "kind": "app", "csp": "chain-wasm", "lhci": true }`. |
|
|
26
|
+
| `mw-network-status` | `mw-network-status [--out src/network.json]`: which networks MeddleWare is deployed on, from the installed clients' deployment records through `networkAvailability` (`@meddleware/web-utils`). A static site runs it as `prebuild`, so "mainnet is coming" turns into "live" by the records alone. |
|
|
27
|
+
| `mw-audit-gate` | CI dependency gate: `npm audit` at high/critical minus the dated, reasoned entries of `.github/audit-allowlist.json` (an entry with no reason or an expired date fails). |
|
|
28
|
+
| `mw-third-party-licenses` | Writes `dist/THIRD_PARTY_LICENSES` from the lockfile's production packages and their installed licence texts; `--check` fails when none can be written. |
|
|
29
|
+
| `mw-check-csp-inline` | `mw-check-csp-inline <policy or @Dockerfile> [dist]`: every inline script in the built pages is allowed by a `sha256` source in the policy, and the policy never allows `'unsafe-inline'` scripts. |
|
|
23
30
|
|
|
24
31
|
## Use
|
|
25
32
|
|
package/dist/csp.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {CspOptions} options
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function buildCsp(options?: CspOptions): string;
|
|
6
|
+
/** The `'sha256-…'` hashes in a policy's script-src (kept when a profile such as `docs` is rebuilt). */
|
|
7
|
+
export function scriptHashesOf(csp: string): string[];
|
|
8
|
+
/** @param {string} name @param {string} [current] the policy now in the Dockerfile (its script hashes are kept) */
|
|
9
|
+
export function cspForProfile(name: string, current?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} CspOptions
|
|
12
|
+
* @property {boolean} [wasm] allow WebAssembly (`'wasm-unsafe-eval'`): the Walrus and Move-bytecode clients
|
|
13
|
+
* @property {boolean} [chain] chain-facing app: https images, https connections (RPC, relay, aggregator and key-server hosts are partly configured), blob workers
|
|
14
|
+
* @property {boolean} [worker] allow `worker-src 'self' blob:` without the rest of `chain` (VitePress sites)
|
|
15
|
+
* @property {boolean} [strict] `base-uri` and `form-action` are `'none'` (a page with no forms and no `<base>`)
|
|
16
|
+
* @property {'self' | 'none'} [frameAncestors] who may frame the page (default `'self'`)
|
|
17
|
+
* @property {string[]} [scriptHashes] `'sha256-…'` sources for the inline scripts a static-site generator emits
|
|
18
|
+
*/
|
|
19
|
+
/** @type {Record<string, CspOptions>} */
|
|
20
|
+
export const PROFILES: Record<string, CspOptions>;
|
|
21
|
+
export type CspOptions = {
|
|
22
|
+
/**
|
|
23
|
+
* allow WebAssembly (`'wasm-unsafe-eval'`): the Walrus and Move-bytecode clients
|
|
24
|
+
*/
|
|
25
|
+
wasm?: boolean | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* chain-facing app: https images, https connections (RPC, relay, aggregator and key-server hosts are partly configured), blob workers
|
|
28
|
+
*/
|
|
29
|
+
chain?: boolean | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* allow `worker-src 'self' blob:` without the rest of `chain` (VitePress sites)
|
|
32
|
+
*/
|
|
33
|
+
worker?: boolean | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* `base-uri` and `form-action` are `'none'` (a page with no forms and no `<base>`)
|
|
36
|
+
*/
|
|
37
|
+
strict?: boolean | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* who may frame the page (default `'self'`)
|
|
40
|
+
*/
|
|
41
|
+
frameAncestors?: "none" | "self" | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* `'sha256-…'` sources for the inline scripts a static-site generator emits
|
|
44
|
+
*/
|
|
45
|
+
scriptHashes?: string[] | undefined;
|
|
46
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @param {{ networkAvailability: Function }} utils
|
|
4
|
+
* @param {Record<string, Record<string, unknown>>} records product name → its deployments object
|
|
5
|
+
*/
|
|
6
|
+
export function networkStatus(utils: {
|
|
7
|
+
networkAvailability: Function;
|
|
8
|
+
}, records: Record<string, Record<string, unknown>>): any;
|
package/dist/sync.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} root repository root
|
|
4
|
+
* @param {{ write?: boolean }} [options]
|
|
5
|
+
* @returns {{ file: string, ok: boolean, note: string }[]}
|
|
6
|
+
*/
|
|
7
|
+
export function sync(root: string, options?: {
|
|
8
|
+
write?: boolean;
|
|
9
|
+
}): {
|
|
10
|
+
file: string;
|
|
11
|
+
ok: boolean;
|
|
12
|
+
note: string;
|
|
13
|
+
}[];
|
package/dist/vite.d.ts
CHANGED
|
@@ -41,3 +41,28 @@ export function themeInit({ default: mode, scriptPath, colors }?: {
|
|
|
41
41
|
* @param {Record<string, string | undefined>} env The environment (default: the process's).
|
|
42
42
|
*/
|
|
43
43
|
export function assertE2eOnlyInE2eMode(mode: string, env?: Record<string, string | undefined>): void;
|
|
44
|
+
/**
|
|
45
|
+
* The Vite configuration every MeddleWare tool app shares: the browserslist build target, the colour mode and season before first paint
|
|
46
|
+
* ({@link themeInit}), the e2e-build guard ({@link assertE2eOnlyInE2eMode}), and `@meddleware/wallet-adapter` excluded from dependency
|
|
47
|
+
* pre-bundling. The wallet adapter ships TS and `.vue` source and holds the shared wallet singleton; pre-bundling would put its TS in
|
|
48
|
+
* `.vite/deps` while its `.vue` files (which esbuild cannot bundle) are served raw and import the raw module, so dev would run two
|
|
49
|
+
* singletons and the guard and the app would see different connection state.
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* import vue from '@vitejs/plugin-vue'
|
|
53
|
+
* export default meddlewareApp({ plugins: [vue()] })
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @param {object} options
|
|
57
|
+
* @param {import('vite').PluginOption[]} options.plugins The app's plugins (`vue()` …); `themeInit` is added after them.
|
|
58
|
+
* @param {'light' | 'dark' | 'system'} [options.theme] The mode a first visit gets (default `dark`, the brand default).
|
|
59
|
+
* @param {boolean} [options.wallet] The app uses `@meddleware/wallet-adapter` (default true): keep it out of pre-bundling.
|
|
60
|
+
* @param {import('vite').UserConfig} [options.config] Anything else (`base`, `build.rollupOptions`, `server` …); `build` is merged.
|
|
61
|
+
* @returns {import('vite').UserConfigFnObject}
|
|
62
|
+
*/
|
|
63
|
+
export function meddlewareApp({ plugins, theme, wallet, config }: {
|
|
64
|
+
plugins: import("vite").PluginOption[];
|
|
65
|
+
theme?: "light" | "dark" | "system" | undefined;
|
|
66
|
+
wallet?: boolean | undefined;
|
|
67
|
+
config?: import("vite").UserConfig | undefined;
|
|
68
|
+
}): import("vite").UserConfigFnObject;
|
package/dist/vitest.d.ts
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
4
4
|
* renders a dialog; the browser's real behaviour (top layer, inert background) is checked by the Playwright suites, not jsdom.
|
|
5
5
|
*/
|
|
6
6
|
export function installDialogShim(): void;
|
|
7
|
+
/**
|
|
8
|
+
* The Vitest configuration of a MeddleWare Vue repository: jsdom, `tests/**\/*.test.ts`, a single copy of Vue for the code under test and
|
|
9
|
+
* `@vue/test-utils`, and the UI packages processed through Vite ({@link inlineMeddleware}).
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import vue from '@vitejs/plugin-vue'
|
|
13
|
+
* export default defineConfig(vueTestConfig({ plugins: [vue()] }))
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {import('vite').PluginOption[]} options.plugins The Vue plugin (and any other the tests need).
|
|
18
|
+
* @param {import('vitest/config').TestUserConfig} [options.test] Overrides merged into `test` (`include`, `setupFiles`, `environment` …).
|
|
19
|
+
* @returns {import('vitest/config').ViteUserConfig & { test: import('vitest/config').TestUserConfig }}
|
|
20
|
+
*/
|
|
21
|
+
export function vueTestConfig({ plugins, test }: {
|
|
22
|
+
plugins: import("vite").PluginOption[];
|
|
23
|
+
test?: import("vitest/node").TestUserConfig | undefined;
|
|
24
|
+
}): import("vitest/config").ViteUserConfig & {
|
|
25
|
+
test: import("vitest/config").TestUserConfig;
|
|
26
|
+
};
|
|
7
27
|
/**
|
|
8
28
|
* Packages to process through Vite in tests: the ones that import CSS or `.vue` files. Vitest matches these against the module's resolved
|
|
9
29
|
* file path (`…/node_modules/@meddleware/ui/dist/…`), not the package name, so the patterns match the path.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/frontend-config",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared frontend tooling for MeddleWare apps: browser support, ESLint, Stylelint, html-validate, PostCSS, Playwright, axe, screenshots, Lighthouse and the Vite target.",
|
|
6
6
|
"license": "0BSD",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"src",
|
|
24
24
|
"dist",
|
|
25
25
|
"CHANGELOG.md",
|
|
26
|
-
"SECURITY.md"
|
|
26
|
+
"SECURITY.md",
|
|
27
|
+
"templates",
|
|
28
|
+
"tsconfig"
|
|
27
29
|
],
|
|
28
30
|
"exports": {
|
|
29
31
|
".": {
|
|
@@ -86,7 +88,12 @@
|
|
|
86
88
|
"./bundle": {
|
|
87
89
|
"types": "./dist/bundle.d.ts",
|
|
88
90
|
"default": "./src/bundle.js"
|
|
89
|
-
}
|
|
91
|
+
},
|
|
92
|
+
"./csp": {
|
|
93
|
+
"types": "./dist/csp.d.ts",
|
|
94
|
+
"default": "./src/csp.js"
|
|
95
|
+
},
|
|
96
|
+
"./tsconfig/vue-app.json": "./tsconfig/vue-app.json"
|
|
90
97
|
},
|
|
91
98
|
"scripts": {
|
|
92
99
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
@@ -164,6 +171,11 @@
|
|
|
164
171
|
"not dead"
|
|
165
172
|
],
|
|
166
173
|
"bin": {
|
|
167
|
-
"check-prod-bundle": "./src/check-prod-bundle.js"
|
|
174
|
+
"check-prod-bundle": "./src/check-prod-bundle.js",
|
|
175
|
+
"mw-audit-gate": "./src/audit-gate.js",
|
|
176
|
+
"mw-third-party-licenses": "./src/third-party-licenses.js",
|
|
177
|
+
"mw-check-csp-inline": "./src/check-csp-inline.js",
|
|
178
|
+
"mw-sync": "./src/sync.js",
|
|
179
|
+
"mw-network-status": "./src/network-status.js"
|
|
168
180
|
}
|
|
169
181
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mw-audit-gate — CI dependency gate: `npm audit` at high/critical, minus advisories listed in
|
|
3
|
+
// .github/audit-allowlist.json. Each entry needs a reason and an expiry date, so an
|
|
4
|
+
// accepted advisory is re-reviewed instead of being ignored forever.
|
|
5
|
+
import { execFileSync } from 'node:child_process'
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
7
|
+
|
|
8
|
+
const ALLOWLIST = '.github/audit-allowlist.json'
|
|
9
|
+
const LEVELS = new Set(['high', 'critical'])
|
|
10
|
+
const GHSA = /^GHSA(-[23456789cfghjmpqrvwx]{4}){3}$/
|
|
11
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/
|
|
12
|
+
|
|
13
|
+
let failed = false
|
|
14
|
+
/** @param {string} msg */
|
|
15
|
+
const fail = (msg) => {
|
|
16
|
+
console.log(`::error::${msg}`)
|
|
17
|
+
failed = true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const entries = existsSync(ALLOWLIST) ? JSON.parse(readFileSync(ALLOWLIST, 'utf8')) : []
|
|
21
|
+
if (!Array.isArray(entries)) throw new Error(`${ALLOWLIST} must be an array`)
|
|
22
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
23
|
+
const allowed = new Map()
|
|
24
|
+
for (const e of entries) {
|
|
25
|
+
if (!GHSA.test(e?.id) || typeof e.reason !== 'string' || !e.reason || !DATE.test(e.expires)) {
|
|
26
|
+
fail(`invalid allowlist entry ${JSON.stringify(e)} (needs id, reason, expires YYYY-MM-DD)`)
|
|
27
|
+
} else if (e.expires < today) {
|
|
28
|
+
fail(`allowlist entry ${e.id} expired on ${e.expires}; re-review it`)
|
|
29
|
+
} else {
|
|
30
|
+
allowed.set(e.id, e)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let raw
|
|
35
|
+
try {
|
|
36
|
+
raw = execFileSync('npm', ['audit', '--json'], { encoding: 'utf8', maxBuffer: 64 << 20 })
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// npm audit exits non-zero whenever it reports anything; the JSON is still on stdout.
|
|
39
|
+
raw = /** @type {{ stdout: string }} */ (err).stdout
|
|
40
|
+
}
|
|
41
|
+
const report = JSON.parse(raw)
|
|
42
|
+
if (report.error) throw new Error(`npm audit failed: ${JSON.stringify(report.error)}`)
|
|
43
|
+
|
|
44
|
+
const found = new Map()
|
|
45
|
+
for (const vuln of Object.values(report.vulnerabilities ?? {})) {
|
|
46
|
+
for (const via of vuln.via) {
|
|
47
|
+
if (typeof via === 'object' && LEVELS.has(via.severity)) {
|
|
48
|
+
found.set(via.url.split('/').pop(), via)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const [id, via] of found) {
|
|
54
|
+
const entry = allowed.get(id)
|
|
55
|
+
if (entry) {
|
|
56
|
+
console.log(`::warning::${id} (${via.name}, ${via.severity}) allowed until ${entry.expires}: ${entry.reason}`)
|
|
57
|
+
} else {
|
|
58
|
+
fail(`${id} (${via.name}, ${via.severity}): ${via.title}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const id of allowed.keys()) {
|
|
62
|
+
if (!found.has(id)) console.log(`::warning::allowlist entry ${id} no longer matches an advisory; remove it`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const open = [...found.keys()].filter((id) => !allowed.has(id)).length
|
|
66
|
+
console.log(`${found.size} high/critical advisories, ${open} not allowlisted`)
|
|
67
|
+
process.exit(failed ? 1 : 0)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fails the build when dist/ contains an inline <script> that the CSP's script-src does not allow, or when script-src allows
|
|
3
|
+
// 'unsafe-inline'. VitePress inlines a couple of small scripts (the appearance/theme bootstrap and the page-hash map); the image serves a
|
|
4
|
+
// hash-based CSP, so a VitePress upgrade (or a change of the `appearance` option) that changes them must update the Dockerfile's CSP ARG
|
|
5
|
+
// in the same change — this check makes that impossible to miss, in CI as well as in the image build.
|
|
6
|
+
//
|
|
7
|
+
// Usage: mw-check-csp-inline "<csp>" [distDir]
|
|
8
|
+
// mw-check-csp-inline @Dockerfile [distDir] (reads the Dockerfile's `ARG CSP="…"` default)
|
|
9
|
+
import { createHash } from 'node:crypto'
|
|
10
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
|
|
13
|
+
const [source, dist = 'dist'] = process.argv.slice(2)
|
|
14
|
+
if (!source) {
|
|
15
|
+
console.error('usage: mw-check-csp-inline "<csp>"|@Dockerfile [distDir]')
|
|
16
|
+
process.exit(2)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let csp = source
|
|
20
|
+
if (source.startsWith('@')) {
|
|
21
|
+
const m = readFileSync(source.slice(1), 'utf8').match(/^ARG CSP="([^"]*)"/m)
|
|
22
|
+
if (!m?.[1]) {
|
|
23
|
+
console.error(`CSP check: no 'ARG CSP="…"' default found in ${source.slice(1)}`)
|
|
24
|
+
process.exit(2)
|
|
25
|
+
}
|
|
26
|
+
csp = m[1]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const scriptSrc = (csp.split(';').map((d) => d.trim()).find((d) => d.startsWith('script-src ')) ?? '').split(/\s+/)
|
|
30
|
+
if (scriptSrc.includes("'unsafe-inline'")) {
|
|
31
|
+
console.error("CSP check: script-src allows 'unsafe-inline'; the page must not.")
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
const allowed = new Set(scriptSrc.filter((t) => t.startsWith("'sha256-")).map((t) => t.slice(1, -1)))
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} dir
|
|
38
|
+
* @returns {Generator<string>}
|
|
39
|
+
*/
|
|
40
|
+
function* htmlFiles(dir) {
|
|
41
|
+
for (const name of readdirSync(dir)) {
|
|
42
|
+
const p = join(dir, name)
|
|
43
|
+
if (statSync(p).isDirectory()) yield* htmlFiles(p)
|
|
44
|
+
else if (name.endsWith('.html')) yield p
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const missing = new Map()
|
|
49
|
+
let files = 0
|
|
50
|
+
for (const file of htmlFiles(dist)) {
|
|
51
|
+
files++
|
|
52
|
+
const html = readFileSync(file, 'utf8')
|
|
53
|
+
for (const m of html.matchAll(/<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/g)) {
|
|
54
|
+
if (!m[1].trim()) continue
|
|
55
|
+
const hash = 'sha256-' + createHash('sha256').update(m[1]).digest('base64')
|
|
56
|
+
if (!allowed.has(hash)) missing.set(hash, file)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (files === 0) {
|
|
60
|
+
console.error(`CSP check: no HTML file found in ${dist}; build first.`)
|
|
61
|
+
process.exit(2)
|
|
62
|
+
}
|
|
63
|
+
if (missing.size) {
|
|
64
|
+
console.error(`CSP check: ${missing.size} inline script(s) not allowed by script-src:`)
|
|
65
|
+
for (const [hash, file] of missing) console.error(` '${hash}' (first seen in ${file})`)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
console.log(`CSP check: every inline script in ${files} HTML file(s) is allowed by script-src.`)
|
package/src/csp.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// The Content-Security-Policy of a MeddleWare static site, built from a named profile. Every frontend image serves one (the Dockerfile's `ARG CSP`,
|
|
3
|
+
// handed to static-server): twelve hand-copied strings drifted in small ways, so the profiles live here and `mw-sync` checks each Dockerfile against its
|
|
4
|
+
// profile. A policy has no 'unsafe-inline' for scripts (`mw-check-csp-inline` fails the build on an inline script that is not allowed by hash).
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} CspOptions
|
|
8
|
+
* @property {boolean} [wasm] allow WebAssembly (`'wasm-unsafe-eval'`): the Walrus and Move-bytecode clients
|
|
9
|
+
* @property {boolean} [chain] chain-facing app: https images, https connections (RPC, relay, aggregator and key-server hosts are partly configured), blob workers
|
|
10
|
+
* @property {boolean} [worker] allow `worker-src 'self' blob:` without the rest of `chain` (VitePress sites)
|
|
11
|
+
* @property {boolean} [strict] `base-uri` and `form-action` are `'none'` (a page with no forms and no `<base>`)
|
|
12
|
+
* @property {'self' | 'none'} [frameAncestors] who may frame the page (default `'self'`)
|
|
13
|
+
* @property {string[]} [scriptHashes] `'sha256-…'` sources for the inline scripts a static-site generator emits
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @type {Record<string, CspOptions>} */
|
|
17
|
+
export const PROFILES = {
|
|
18
|
+
/** Same-origin only: the landing page. */
|
|
19
|
+
static: {},
|
|
20
|
+
/** Same-origin only, no forms and no `<base>`: the status page and the public roadmap. */
|
|
21
|
+
'static-strict': { strict: true },
|
|
22
|
+
/** An operator console: same-origin only and never framed. */
|
|
23
|
+
console: { frameAncestors: 'none' },
|
|
24
|
+
/** A chain-facing tool: https connections and images, blob workers. */
|
|
25
|
+
chain: { chain: true },
|
|
26
|
+
/** A chain-facing tool that runs WebAssembly (Walrus, Move bytecode). */
|
|
27
|
+
'chain-wasm': { chain: true, wasm: true },
|
|
28
|
+
/** A VitePress site: same-origin connections, https images, blob workers, inline scripts allowed by hash (the hashes are kept from the current policy). */
|
|
29
|
+
docs: { worker: true },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {CspOptions} options
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
export function buildCsp(options = {}) {
|
|
37
|
+
const { wasm = false, chain = false, worker = false, strict = false, frameAncestors = 'self', scriptHashes = [] } = options
|
|
38
|
+
const script = ["'self'", ...(wasm ? ["'wasm-unsafe-eval'"] : []), ...scriptHashes.map((h) => `'${h.replace(/^'|'$/g, '')}'`)]
|
|
39
|
+
const richImages = chain || worker
|
|
40
|
+
const directives = [
|
|
41
|
+
["default-src", "'self'"],
|
|
42
|
+
['script-src', script.join(' ')],
|
|
43
|
+
['style-src', "'self' 'unsafe-inline'"],
|
|
44
|
+
['img-src', richImages ? "'self' data: blob: https:" : "'self' data:"],
|
|
45
|
+
['font-src', "'self' data:"],
|
|
46
|
+
['connect-src', chain ? "'self' https:" : "'self'"],
|
|
47
|
+
...(chain || worker ? [['worker-src', "'self' blob:"]] : []),
|
|
48
|
+
['object-src', "'none'"],
|
|
49
|
+
['base-uri', strict ? "'none'" : "'self'"],
|
|
50
|
+
['form-action', strict ? "'none'" : "'self'"],
|
|
51
|
+
['frame-ancestors', `'${frameAncestors}'`],
|
|
52
|
+
]
|
|
53
|
+
return [...directives.map(([d, v]) => `${d} ${v}`), 'upgrade-insecure-requests'].join('; ')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The `'sha256-…'` hashes in a policy's script-src (kept when a profile such as `docs` is rebuilt). */
|
|
57
|
+
export function scriptHashesOf(/** @type {string} */ csp) {
|
|
58
|
+
const src = csp.split(';').map((d) => d.trim()).find((d) => d.startsWith('script-src ')) ?? ''
|
|
59
|
+
return src.split(/\s+/).filter((t) => t.startsWith("'sha256-")).map((t) => t.slice(1, -1))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @param {string} name @param {string} [current] the policy now in the Dockerfile (its script hashes are kept) */
|
|
63
|
+
export function cspForProfile(name, current = '') {
|
|
64
|
+
const profile = PROFILES[name]
|
|
65
|
+
if (!profile) throw new Error(`unknown CSP profile "${name}" (known: ${Object.keys(PROFILES).join(', ')})`)
|
|
66
|
+
return buildCsp({ ...profile, ...(name === 'docs' ? { scriptHashes: scriptHashesOf(current) } : {}) })
|
|
67
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
// mw-network-status — write which networks MeddleWare is deployed on, from the deployment records of the installed client packages, so a static
|
|
4
|
+
// site (landing page, docs, developer docs) says "mainnet is coming" until the records have a `mainnet` key and says "live" the moment they do.
|
|
5
|
+
//
|
|
6
|
+
// mw-network-status [--out src/network.json] (default: print to stdout)
|
|
7
|
+
//
|
|
8
|
+
// Run it from the repository root before the build (a `prebuild` script). The repository needs `@meddleware/web-utils` and the two clients
|
|
9
|
+
// (`@meddleware/access-gate-client`, `@meddleware/sealed-storage-client`) installed; it fails when one is missing rather than guess.
|
|
10
|
+
import { writeFileSync } from 'node:fs'
|
|
11
|
+
import { createRequire } from 'node:module'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { pathToFileURL } from 'node:url'
|
|
14
|
+
|
|
15
|
+
/** @param {string} name */
|
|
16
|
+
async function load(name) {
|
|
17
|
+
const require = createRequire(join(process.cwd(), 'noop.js'))
|
|
18
|
+
return import(pathToFileURL(require.resolve(name)).href)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {{ networkAvailability: Function }} utils
|
|
23
|
+
* @param {Record<string, Record<string, unknown>>} records product name → its deployments object
|
|
24
|
+
*/
|
|
25
|
+
export function networkStatus(utils, records) {
|
|
26
|
+
return utils.networkAvailability(Object.fromEntries(Object.entries(records).map(([name, deployments]) => [name, Object.keys(deployments)])))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
30
|
+
const args = process.argv.slice(2)
|
|
31
|
+
const outIndex = args.indexOf('--out')
|
|
32
|
+
const out = outIndex >= 0 ? args[outIndex + 1] : undefined
|
|
33
|
+
try {
|
|
34
|
+
const utils = await load('@meddleware/web-utils')
|
|
35
|
+
const gate = await load('@meddleware/access-gate-client')
|
|
36
|
+
const sealed = await load('@meddleware/sealed-storage-client')
|
|
37
|
+
const status = networkStatus(utils, { 'access gate': gate.ACCESS_GATE_DEPLOYMENTS, 'sealed storage': sealed.SEAL_POLICIES_DEPLOYMENTS })
|
|
38
|
+
const json = JSON.stringify(status, null, 2) + '\n'
|
|
39
|
+
if (out) writeFileSync(out, json)
|
|
40
|
+
else process.stdout.write(json)
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(`mw-network-status: ${err instanceof Error ? err.message : String(err)}`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/sync.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
// mw-sync — keep the files that every MeddleWare frontend repository copies from this package identical to the package's templates, and the
|
|
4
|
+
// Dockerfile's Content-Security-Policy equal to its named profile. Run in a repository root:
|
|
5
|
+
//
|
|
6
|
+
// mw-sync --check fail (exit 1) when a file differs; print which and how to fix
|
|
7
|
+
// mw-sync --write overwrite the files with the templates / the profile's policy
|
|
8
|
+
//
|
|
9
|
+
// The repository declares what it is in package.json:
|
|
10
|
+
// "meddleware": { "kind": "app", "csp": "chain-wasm", "lhci": true }
|
|
11
|
+
// kind "app" → .github/dependabot.yml, .dockerignore, postcss.config.js, stylelint.config.js are the templates
|
|
12
|
+
// csp a profile of ./csp.js → the Dockerfile's `ARG CSP="…"` is that profile's policy (`docs` keeps the script hashes already there)
|
|
13
|
+
// lhci true → .github/audit-allowlist.json is the template (the dated allowlist for the Lighthouse CI tool chain; one expiry, reviewed together)
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
15
|
+
import { dirname, join } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
import { cspForProfile } from './csp.js'
|
|
18
|
+
|
|
19
|
+
const templates = join(dirname(fileURLToPath(import.meta.url)), '..', 'templates')
|
|
20
|
+
const tpl = (/** @type {string} */ name) => readFileSync(join(templates, name), 'utf8')
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} root repository root
|
|
24
|
+
* @param {{ write?: boolean }} [options]
|
|
25
|
+
* @returns {{ file: string, ok: boolean, note: string }[]}
|
|
26
|
+
*/
|
|
27
|
+
export function sync(root, options = {}) {
|
|
28
|
+
const pkgPath = join(root, 'package.json')
|
|
29
|
+
const pkg = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, 'utf8')) : {}
|
|
30
|
+
const cfg = pkg.meddleware ?? {}
|
|
31
|
+
/** @type {{ file: string, ok: boolean, note: string }[]} */
|
|
32
|
+
const out = []
|
|
33
|
+
/** @param {string} file @param {string} want */
|
|
34
|
+
const expectFile = (file, want) => {
|
|
35
|
+
const p = join(root, file)
|
|
36
|
+
const have = existsSync(p) ? readFileSync(p, 'utf8') : null
|
|
37
|
+
if (have === want) return out.push({ file, ok: true, note: 'matches the template' })
|
|
38
|
+
if (options.write) {
|
|
39
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
40
|
+
writeFileSync(p, want)
|
|
41
|
+
return out.push({ file, ok: true, note: have === null ? 'created' : 'rewritten' })
|
|
42
|
+
}
|
|
43
|
+
out.push({ file, ok: false, note: have === null ? 'missing (mw-sync --write creates it)' : 'differs from the template (mw-sync --write restores it)' })
|
|
44
|
+
}
|
|
45
|
+
if (cfg.kind === 'app') {
|
|
46
|
+
expectFile('.github/dependabot.yml', tpl('dependabot.app.yml'))
|
|
47
|
+
expectFile('.dockerignore', tpl('dockerignore'))
|
|
48
|
+
expectFile('postcss.config.js', tpl('postcss.config.js'))
|
|
49
|
+
expectFile('stylelint.config.js', tpl('stylelint.config.js'))
|
|
50
|
+
}
|
|
51
|
+
if (cfg.lhci === true) expectFile('.github/audit-allowlist.json', tpl('audit-allowlist.lhci.json'))
|
|
52
|
+
if (cfg.csp) {
|
|
53
|
+
const df = join(root, 'Dockerfile')
|
|
54
|
+
const text = existsSync(df) ? readFileSync(df, 'utf8') : ''
|
|
55
|
+
const m = text.match(/^ARG CSP="([^"]*)"/m)
|
|
56
|
+
if (!m) out.push({ file: 'Dockerfile', ok: false, note: `no 'ARG CSP="…"' default found (profile ${cfg.csp})` })
|
|
57
|
+
else {
|
|
58
|
+
const want = cspForProfile(cfg.csp, m[1])
|
|
59
|
+
if (m[1] === want) out.push({ file: 'Dockerfile', ok: true, note: `CSP equals the "${cfg.csp}" profile` })
|
|
60
|
+
else if (options.write) {
|
|
61
|
+
writeFileSync(df, text.replace(/^ARG CSP="[^"]*"/m, () => `ARG CSP="${want}"`))
|
|
62
|
+
out.push({ file: 'Dockerfile', ok: true, note: `CSP rewritten to the "${cfg.csp}" profile` })
|
|
63
|
+
} else out.push({ file: 'Dockerfile', ok: false, note: `CSP differs from the "${cfg.csp}" profile:\n have ${m[1]}\n want ${want}` })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (out.length === 0) out.push({ file: 'package.json', ok: false, note: 'no "meddleware" section: nothing to check (add kind / csp / lhci)' })
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
71
|
+
const args = process.argv.slice(2)
|
|
72
|
+
const write = args.includes('--write')
|
|
73
|
+
if (!write && !args.includes('--check')) {
|
|
74
|
+
console.error('usage: mw-sync --check | --write')
|
|
75
|
+
process.exit(2)
|
|
76
|
+
}
|
|
77
|
+
const results = sync(process.cwd(), { write })
|
|
78
|
+
for (const r of results) console.log(`${r.ok ? 'ok ' : 'FAIL'} ${r.file}: ${r.note}`)
|
|
79
|
+
process.exit(results.every((r) => r.ok) ? 0 : 1)
|
|
80
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Writes dist/THIRD_PARTY_LICENSES: the licence and NOTICE texts of every production dependency in
|
|
3
|
+
// package-lock.json. The site ships a bundle, so the packages' own files do not travel with it;
|
|
4
|
+
// Apache-2.0 (§4), MIT and BSD all require the notice to accompany a redistribution. The file is served
|
|
5
|
+
// with the site (/THIRD_PARTY_LICENSES) and read by licence audits.
|
|
6
|
+
//
|
|
7
|
+
// mw-third-party-licenses write the file
|
|
8
|
+
// mw-third-party-licenses --check also fail when the file would be empty
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
|
|
12
|
+
// The project root is the directory the command runs in (an app's npm script), not this package.
|
|
13
|
+
const root = process.cwd()
|
|
14
|
+
const lock = JSON.parse(readFileSync(join(root, 'package-lock.json'), 'utf8'))
|
|
15
|
+
const check = process.argv.includes('--check')
|
|
16
|
+
const LICENSE_FILE = /^(licen[cs]e|notice|copying|unlicense)(\.|$|-)/i
|
|
17
|
+
|
|
18
|
+
const out = []
|
|
19
|
+
const missing = []
|
|
20
|
+
const apacheMissing = []
|
|
21
|
+
/** @param {any} pkg */
|
|
22
|
+
const declared = (pkg) => (typeof pkg.license === 'string' ? pkg.license : JSON.stringify(pkg.license ?? pkg.licenses ?? 'unknown'))
|
|
23
|
+
/** @param {any} pkg */
|
|
24
|
+
const holder = (pkg) =>
|
|
25
|
+
[pkg.author && (typeof pkg.author === 'string' ? pkg.author : pkg.author.name), pkg.repository && (typeof pkg.repository === 'string' ? pkg.repository : pkg.repository.url)]
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join(' · ')
|
|
28
|
+
const names = Object.keys(lock.packages)
|
|
29
|
+
.filter((p) => p.startsWith('node_modules/') && !lock.packages[p].dev && !lock.packages[p].optional)
|
|
30
|
+
.sort()
|
|
31
|
+
for (const p of names) {
|
|
32
|
+
const dir = join(root, p)
|
|
33
|
+
if (!existsSync(dir)) continue
|
|
34
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
|
|
35
|
+
const files = readdirSync(dir).filter((f) => LICENSE_FILE.test(f)).sort()
|
|
36
|
+
out.push(`${'='.repeat(78)}\n${pkg.name}@${pkg.version} (${declared(pkg)})\n${'='.repeat(78)}`)
|
|
37
|
+
if (files.length === 0) {
|
|
38
|
+
missing.push(`${pkg.name}@${pkg.version}`)
|
|
39
|
+
if (/Apache-2\.0/.test(declared(pkg))) apacheMissing.push(`${pkg.name}@${pkg.version}`)
|
|
40
|
+
}
|
|
41
|
+
for (const f of files) out.push(`--- ${f} ---\n${readFileSync(join(dir, f), 'utf8').trim()}\n`)
|
|
42
|
+
if (files.length === 0) {
|
|
43
|
+
const apache = /Apache-2\.0/.test(declared(pkg))
|
|
44
|
+
out.push(`(the package ships no licence file; declared licence: ${declared(pkg)}${holder(pkg) ? `; ${holder(pkg)}` : ''}${apache ? '; the Apache License 2.0 text follows at the end of this file' : ''})\n`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Apache-2.0 §4(a) asks that a recipient gets a copy of the License. A package that declares it but ships no
|
|
48
|
+
// file is covered by the canonical text, taken from any installed package that carries it.
|
|
49
|
+
if (apacheMissing.length > 0) {
|
|
50
|
+
let text = null
|
|
51
|
+
for (const p of Object.keys(lock.packages).filter((k) => k.startsWith('node_modules/'))) {
|
|
52
|
+
const dir = join(root, p)
|
|
53
|
+
if (!existsSync(dir)) continue
|
|
54
|
+
for (const f of readdirSync(dir).filter((x) => LICENSE_FILE.test(x))) {
|
|
55
|
+
const t = readFileSync(join(dir, f), 'utf8')
|
|
56
|
+
if (/Apache License\s+Version 2\.0, January 2004/.test(t) && /END OF TERMS AND CONDITIONS/.test(t)) {
|
|
57
|
+
text = t.slice(0, t.indexOf('END OF TERMS AND CONDITIONS') + 'END OF TERMS AND CONDITIONS'.length).trim()
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (text) break
|
|
62
|
+
}
|
|
63
|
+
out.push(`${'='.repeat(78)}\nApache License 2.0 — applies to: ${apacheMissing.join(', ')}\n${'='.repeat(78)}`)
|
|
64
|
+
out.push(text ?? '(canonical text not found in the installed packages: see https://www.apache.org/licenses/LICENSE-2.0)')
|
|
65
|
+
if (check && text === null) {
|
|
66
|
+
console.error('Apache-2.0 packages without a licence file, and no canonical Apache License text installed')
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
mkdirSync(join(root, 'dist'), { recursive: true })
|
|
71
|
+
writeFileSync(join(root, 'dist', 'THIRD_PARTY_LICENSES'), `${out.join('\n')}\n`)
|
|
72
|
+
console.log(`THIRD_PARTY_LICENSES: ${names.length} production packages, ${missing.length} without a licence file`)
|
|
73
|
+
if (missing.length) console.log(` no licence file: ${missing.join(', ')}`)
|
|
74
|
+
if (check && out.length === 0) {
|
|
75
|
+
console.error('no production packages found in package-lock.json')
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
package/src/vite.js
CHANGED
|
@@ -85,3 +85,34 @@ export function assertE2eOnlyInE2eMode(mode, env = process.env) {
|
|
|
85
85
|
throw new Error(`VITE_E2E is set but the build mode is "${mode}": the end-to-end build is \`vite build --mode e2e\`, and nothing else may include the test wallet.`)
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The Vite configuration every MeddleWare tool app shares: the browserslist build target, the colour mode and season before first paint
|
|
91
|
+
* ({@link themeInit}), the e2e-build guard ({@link assertE2eOnlyInE2eMode}), and `@meddleware/wallet-adapter` excluded from dependency
|
|
92
|
+
* pre-bundling. The wallet adapter ships TS and `.vue` source and holds the shared wallet singleton; pre-bundling would put its TS in
|
|
93
|
+
* `.vite/deps` while its `.vue` files (which esbuild cannot bundle) are served raw and import the raw module, so dev would run two
|
|
94
|
+
* singletons and the guard and the app would see different connection state.
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* import vue from '@vitejs/plugin-vue'
|
|
98
|
+
* export default meddlewareApp({ plugins: [vue()] })
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @param {object} options
|
|
102
|
+
* @param {import('vite').PluginOption[]} options.plugins The app's plugins (`vue()` …); `themeInit` is added after them.
|
|
103
|
+
* @param {'light' | 'dark' | 'system'} [options.theme] The mode a first visit gets (default `dark`, the brand default).
|
|
104
|
+
* @param {boolean} [options.wallet] The app uses `@meddleware/wallet-adapter` (default true): keep it out of pre-bundling.
|
|
105
|
+
* @param {import('vite').UserConfig} [options.config] Anything else (`base`, `build.rollupOptions`, `server` …); `build` is merged.
|
|
106
|
+
* @returns {import('vite').UserConfigFnObject}
|
|
107
|
+
*/
|
|
108
|
+
export function meddlewareApp({ plugins, theme = 'dark', wallet = true, config = {} }) {
|
|
109
|
+
return ({ mode }) => {
|
|
110
|
+
assertE2eOnlyInE2eMode(mode)
|
|
111
|
+
return {
|
|
112
|
+
...config,
|
|
113
|
+
build: { target: viteTarget(), ...config.build },
|
|
114
|
+
plugins: [...plugins, themeInit({ default: theme })],
|
|
115
|
+
...(wallet ? { optimizeDeps: { ...config.optimizeDeps, exclude: ['@meddleware/wallet-adapter', ...(config.optimizeDeps?.exclude ?? [])] } } : {}),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/vitest.js
CHANGED
|
@@ -50,3 +50,32 @@ export function installDialogShim() {
|
|
|
50
50
|
this.dispatchEvent(new Event('close'))
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The Vitest configuration of a MeddleWare Vue repository: jsdom, `tests/**\/*.test.ts`, a single copy of Vue for the code under test and
|
|
56
|
+
* `@vue/test-utils`, and the UI packages processed through Vite ({@link inlineMeddleware}).
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* import vue from '@vitejs/plugin-vue'
|
|
60
|
+
* export default defineConfig(vueTestConfig({ plugins: [vue()] }))
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @param {object} options
|
|
64
|
+
* @param {import('vite').PluginOption[]} options.plugins The Vue plugin (and any other the tests need).
|
|
65
|
+
* @param {import('vitest/config').TestUserConfig} [options.test] Overrides merged into `test` (`include`, `setupFiles`, `environment` …).
|
|
66
|
+
* @returns {import('vitest/config').ViteUserConfig & { test: import('vitest/config').TestUserConfig }}
|
|
67
|
+
*/
|
|
68
|
+
export function vueTestConfig({ plugins, test = {} }) {
|
|
69
|
+
const inline = test.server?.deps?.inline
|
|
70
|
+
return {
|
|
71
|
+
plugins,
|
|
72
|
+
resolve: { dedupe: ['vue', '@vue/test-utils'] },
|
|
73
|
+
test: {
|
|
74
|
+
environment: 'jsdom',
|
|
75
|
+
include: ['tests/**/*.test.ts'],
|
|
76
|
+
exclude: ['node_modules/**'],
|
|
77
|
+
...test,
|
|
78
|
+
server: { ...test.server, deps: { ...test.server?.deps, inline: inline === true ? true : [...inlineMeddleware, ...(inline ?? [])] } },
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "GHSA-jmr9-qjv8-65gv",
|
|
4
|
+
"reason": "extract-zip has no patched release. It is a dev-only transitive of @lhci/cli (lighthouse > puppeteer-core > @puppeteer/browsers) and only unpacks browser archives that Lighthouse CI downloads itself; nothing from it ships in the image.",
|
|
5
|
+
"expires": "2027-01-31"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"id": "GHSA-7pqw-9j4j-h8q3",
|
|
9
|
+
"reason": "extract-zip has no patched release. It is a dev-only transitive of @lhci/cli (lighthouse > puppeteer-core > @puppeteer/browsers) and only unpacks browser archives that Lighthouse CI downloads itself; nothing from it ships in the image.",
|
|
10
|
+
"expires": "2027-01-31"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: npm
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
open-pull-requests-limit: 5
|
|
8
|
+
ignore:
|
|
9
|
+
- dependency-name: "typescript"
|
|
10
|
+
update-types: ["version-update:semver-major"]
|
|
11
|
+
groups:
|
|
12
|
+
npm-minor-patch:
|
|
13
|
+
update-types: [minor, patch]
|
|
14
|
+
- package-ecosystem: docker
|
|
15
|
+
directory: /
|
|
16
|
+
schedule:
|
|
17
|
+
interval: weekly
|
|
18
|
+
open-pull-requests-limit: 5
|
|
19
|
+
ignore:
|
|
20
|
+
- dependency-name: "golang"
|
|
21
|
+
update-types: ["version-update:semver-minor", "version-update:semver-major"]
|
|
22
|
+
groups:
|
|
23
|
+
docker:
|
|
24
|
+
patterns: ['*']
|
|
25
|
+
- package-ecosystem: github-actions
|
|
26
|
+
directory: /
|
|
27
|
+
schedule:
|
|
28
|
+
interval: weekly
|
|
29
|
+
open-pull-requests-limit: 5
|
|
30
|
+
groups:
|
|
31
|
+
github-actions:
|
|
32
|
+
patterns: ['*']
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Build context: sources only. The image runs its own `npm ci` and production build, so local
|
|
2
|
+
# installs and build output never enter it — and local-only env files (Vite loads `.env*.local` in
|
|
3
|
+
# every mode) can never be baked into the production bundle. Committed `.env` / `.env.production`
|
|
4
|
+
# defaults stay in the context on purpose.
|
|
5
|
+
node_modules
|
|
6
|
+
dist
|
|
7
|
+
.git
|
|
8
|
+
.github
|
|
9
|
+
.env*.local
|
|
10
|
+
*.log
|
|
11
|
+
test-results
|
|
12
|
+
playwright-report
|
|
13
|
+
coverage
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@meddleware/frontend-config/postcss'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"types": ["node"],
|
|
16
|
+
"noEmit": true
|
|
17
|
+
}
|
|
18
|
+
}
|