Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/workflows/security-release-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,18 @@ jobs:
run: |
set -euo pipefail
npm ci --ignore-scripts --omit=optional --no-audit --no-fund
npm run test:security
npm audit --omit=dev --json > evidence/npm-audit-runtime.json
npm audit --json > evidence/npm-audit-all.json
npm run ci
npm run ci | tee evidence/ci-validation.log
version=$(node -p "require('./package.json').version")
artifact="dist/${version}.tar.gz"
cp "$artifact" /tmp/api-explorer-first.tar.gz
rm -rf dist tmp
npm run build
cmp /tmp/api-explorer-first.tar.gz "$artifact"
# scripts/ci already compares two complete builds byte for byte.
# Retain that exact tested archive before runner cleanup.
cp "$artifact" "evidence/api-explorer-${version}.tar.gz"
cp THIRD-PARTY-NOTICES.md evidence/
sha256sum "$artifact" > evidence/product-artifact.sha256
tar -tzf "$artifact" > evidence/product-members.txt
(cd evidence && sha256sum "api-explorer-${version}.tar.gz" > SHA256SUMS)

- name: Generate package SBOM
shell: bash
Expand All @@ -79,6 +79,7 @@ jobs:
--output-format JSON \
--spec-version 1.6 \
--output-file evidence/api-explorer.cdx.json
(cd evidence && sha256sum api-explorer.cdx.json >> SHA256SUMS)

- name: Scan immutable source and product
shell: bash
Expand Down Expand Up @@ -117,6 +118,11 @@ jobs:
jq -e '.metadata.vulnerabilities.critical == 0 and .metadata.vulnerabilities.high == 0' evidence/npm-audit-runtime.json >/dev/null
jq -e '.metadata.vulnerabilities.critical == 0 and .metadata.vulnerabilities.high == 0' evidence/npm-audit-all.json >/dev/null
jq -e '.bomFormat == "CycloneDX" and .specVersion == "1.6" and (.components | length) > 0' evidence/api-explorer.cdx.json >/dev/null
(cd evidence && sha256sum source-security.json product-security.json \
npm-audit-runtime.json npm-audit-all.json security-summary.txt \
THIRD-PARTY-NOTICES.md source-revision.txt source-locks.sha256 \
node-version.txt npm-version.txt product-artifact.sha256 product-members.txt \
ci-validation.log >> SHA256SUMS)

- name: Upload reviewed evidence
if: always()
Expand Down
12 changes: 10 additions & 2 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@ API Explorer preserves schema discovery, resource links, collection and action f

Visible product naming and package ownership use PastureStack. Historical API resource names, schema fields, enum values, response links, and compatibility CSS hooks remain unchanged where supplied by or coupled to the remote API.

The Server packaging contract currently consumes the `1.1.15` static archive and exposes its `ui.min.js` and `ui.min.css` files below `/api-ui/`. The `1.1.16` security candidate preserves that archive layout; changing the Server default still requires a coordinated embedded-server test.
Version `1.1.19` preserves the static archive layout, including `ui.min.js`,
`ui.min.css`, fonts, licenses and `version.json`, consumed by Server below
`/api-ui/`. Server pins its own reviewed component version; changing that pin
requires a coordinated embedded-server test.

The release keeps the reviewed Bootstrap 3.4.1 CSS and font layout contract as
The release keeps the reviewed Bootstrap 5.3.8 CSS and Bootstrap Icons 1.13.1 as
static, attributed assets. It does not load Bootstrap JavaScript. A small
PastureStack-owned compatibility layer implements only the modal and filter
dropdown behavior used by this explorer. Native `title` text provides field
help; executable tooltip, popover, button, and generic Bootstrap data APIs are
not part of the artifact.

Handlebars 4.7.10 precompiles tracked template and partial strings during the
build. The browser receives the runtime only; API input never enters a template
compiler. Official CVE regressions, precompiled partials, escaped API text and
both locales are checked without changing remote field or error values.

Before release, validate read-only and mutating resources, actions, pagination, filters, authentication failures, HTML embedding, escaping, cross-origin behavior, and `en-US`/`zh-TW` selection against an isolated compatible server.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ PastureStack is an independent community effort to preserve, audit, and moderniz

This maintained compatibility release is based on the latest reviewed upstream `master`, including current Node and dependency maintenance. Package metadata, visible branding, and repository links use PastureStack naming. Existing API schema fields and discovery behavior remain compatible.

The current immutable release is [`v1.1.18`](https://github.com/PastureStack/api-explorer/releases/tag/v1.1.18).
The current release is [`v1.1.19`](https://github.com/PastureStack/api-explorer/releases/tag/v1.1.19).
The explorer is supported only as a Server-embedded compatibility interface;
this repository does not publish a standalone CDN or npm package. Use the
[Server release documentation](https://github.com/PastureStack/server#readme)
Expand All @@ -23,12 +23,15 @@ Requires Node.js 24.20.0 LTS and npm 12.0.2.

```sh
npm ci --no-audit --no-fund
npm run test:security
npm run ci
npm run build
```

The tracked npm lockfile is the executable dependency contract. The build emits a deterministic `dist/1.1.18.tar.gz`: file order, modes, ownership, timestamps, and the gzip header are normalized from the source commit. Source maps are excluded from the embedded release artifact.
The tracked npm lockfile is the executable dependency contract. The CI command runs security and product tests and compares two complete builds byte for byte. The build emits a deterministic `dist/1.1.19.tar.gz`: file order, modes, ownership, timestamps, and the gzip header are normalized from the source commit. Source maps are excluded from the embedded release artifact. The security release gate retains the tested archive, SBOM, scans and SHA256SUMS for publication without rebuilding.

Handlebars 4.7.10 supplies the official fixes for CVE-2026-106444,
CVE-2026-106445 and CVE-2026-106446. Templates and partials are compiled from
tracked source during the build; only the runtime and precompiled templates
are shipped to the browser. See the [upstream security release](https://github.com/handlebars-lang/handlebars.js/releases/tag/v4.7.10).

Bootstrap JavaScript is not shipped. The artifact uses reviewed, attributed
Bootstrap 5.3.8 CSS and Bootstrap Icons 1.13.1, plus a small first-party modal
Expand Down
5 changes: 4 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

## Supported state

Version `1.1.17` remains the supported Server-embedded compatibility release. Version `1.1.18` is the reviewed dependency-refresh candidate until the coordinated Server integration gate passes.
Version `1.1.19` updates the build compiler and bundled runtime to Handlebars
4.7.10, including the official fixes for CVE-2026-106444, CVE-2026-106445 and
CVE-2026-106446. Server-embedded support follows the component version pinned
by the coordinated Server release and its integration gate.

## Security boundaries

Expand Down
4 changes: 2 additions & 2 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ and other subsequent modifications.

- jQuery 4.0.0, copyright OpenJS Foundation and other contributors, MIT.
The complete text is emitted as `licenses/jquery-4.0.0/LICENSE.txt`.
- Handlebars Runtime 4.7.9, copyright Yehuda Katz and contributors, MIT.
The complete text is emitted as `licenses/handlebars-4.7.9/LICENSE`.
- Handlebars Runtime 4.7.10, copyright Yehuda Katz and contributors, MIT.
The complete text is emitted as `licenses/handlebars-4.7.10/LICENSE`.
- async 0.9.0, copyright Caolan McMahon, MIT. The retained text is
[`vendor/licenses/LICENSE-async-0.9.0`](vendor/licenses/LICENSE-async-0.9.0).
- jquery-scrollintoview 1.8, copyright Robert Koritnik, MIT. The retained text
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pasturestack/api-explorer",
"version": "1.1.18",
"version": "1.1.19",
"private": true,
"description": "Embedded PastureStack explorer for compatible API services",
"author": "Original contributors and PastureStack maintainers",
Expand All @@ -19,7 +19,8 @@
"clean": "bash ./scripts/clean",
"build": "bash ./scripts/build",
"ci": "bash ./scripts/ci",
"test:security": "node --test test/security/*.test.js"
"test:security": "node --test test/security/*.test.js",
"test:product": "node --test test/product/*.test.js"
},
"devDependencies": {
"bootstrap": "5.3.8",
Expand All @@ -30,7 +31,7 @@
"uglify-js": "3.19.3"
},
"dependencies": {
"handlebars": "4.7.9",
"handlebars": "4.7.10",
"jquery": "4.0.0"
}
}
4 changes: 2 additions & 2 deletions scripts/build
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ mkdir -p \
"${DIST}/licenses/bootstrap-5.3.8" \
"${DIST}/licenses/bootstrap-icons-1.13.1" \
"${DIST}/licenses/jquery-4.0.0" \
"${DIST}/licenses/handlebars-4.7.9" \
"${DIST}/licenses/handlebars-4.7.10" \
"${DIST}/licenses/inherited-vendor"
cp node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff \
node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2 \
Expand All @@ -98,7 +98,7 @@ cp node_modules/bootstrap/LICENSE "${DIST}/licenses/bootstrap-5.3.8/LICENSE"
cp node_modules/bootstrap-icons/LICENSE \
"${DIST}/licenses/bootstrap-icons-1.13.1/LICENSE"
cp node_modules/jquery/LICENSE.txt "${DIST}/licenses/jquery-4.0.0/LICENSE.txt"
cp node_modules/handlebars/LICENSE "${DIST}/licenses/handlebars-4.7.9/LICENSE"
cp node_modules/handlebars/LICENSE "${DIST}/licenses/handlebars-4.7.10/LICENSE"
cp vendor/licenses/LICENSE-* vendor/licenses/NOTICE-* \
"${DIST}/licenses/inherited-vendor/"
find "${DIST}/licenses" -type f -exec sed -i 's/\r$//' {} +
Expand Down
18 changes: 10 additions & 8 deletions scripts/ci
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function expect(section, name, expected) {
}
}
expect('dependencies', 'jquery', '4.0.0');
expect('dependencies', 'handlebars', '4.7.9');
expect('dependencies', 'handlebars', '4.7.10');
expect('devDependencies', 'bootstrap', '5.3.8');
expect('devDependencies', 'bootstrap-icons', '1.13.1');
expect('devDependencies', 'sass', '1.103.1');
Expand All @@ -35,7 +35,7 @@ for (const section of ['dependencies', 'optionalDependencies']) {
}
}
}
if (pkg.version !== '1.1.18') {
if (pkg.version !== '1.1.19') {
fail('PACKAGE_RELEASE_VERSION_UNEXPECTED', `actual=${pkg.version}`);
}
if (!pkg.engines || pkg.engines.node !== '24.20.0') {
Expand All @@ -54,7 +54,7 @@ if (failures) {
console.log(`failure_count=${failures}`);
process.exit(1);
}
console.log('API_EXPLORER_PACKAGE_METADATA_OK node=24.20.0 npm=12.0.2 jquery=4.0.0 handlebars=4.7.9 bootstrap_css=5.3.8 bootstrap_icons=1.13.1 sass=1.103.1 jsdom=30.0.1 uglify-js=3.19.3 clean-css=5.3.3');
console.log('API_EXPLORER_PACKAGE_METADATA_OK node=24.20.0 npm=12.0.2 jquery=4.0.0 handlebars=4.7.10 bootstrap_css=5.3.8 bootstrap_icons=1.13.1 sass=1.103.1 jsdom=30.0.1 uglify-js=3.19.3 clean-css=5.3.3');
NODE

test "$(node -p "require('bootstrap/package.json').version")" = 5.3.8
Expand Down Expand Up @@ -131,7 +131,7 @@ test -s "$release_dir/licenses/THIRD-PARTY-NOTICES.md"
test -s "$release_dir/licenses/bootstrap-5.3.8/LICENSE"
test -s "$release_dir/licenses/bootstrap-icons-1.13.1/LICENSE"
test -s "$release_dir/licenses/jquery-4.0.0/LICENSE.txt"
test -s "$release_dir/licenses/handlebars-4.7.9/LICENSE"
test -s "$release_dir/licenses/handlebars-4.7.10/LICENSE"
test -s "$release_dir/licenses/inherited-vendor/LICENSE-async-0.9.0"
test -s "$release_dir/licenses/inherited-vendor/LICENSE-jquery-scrollintoview-1.8"
test -s "$release_dir/licenses/inherited-vendor/LICENSE-jsonformatter-2009"
Expand All @@ -154,7 +154,8 @@ if grep -Fq 'Bootstrap v3.' "$release_dir/ui.css"; then
fi

member_list=$(mktemp)
trap 'rm -f "$member_list"' EXIT
first_archive=$(mktemp)
trap 'rm -f "$member_list" "$first_archive"' EXIT
tar -tzf "$artifact" > "$member_list"
if grep -E '(^/|(^|/)\.\.(/|$)|\.map$|(^|/)js/bootstrap)' "$member_list"; then
echo 'API_EXPLORER_RELEASE_ARCHIVE_UNSAFE_OR_REJECTED_MEMBER'
Expand All @@ -171,7 +172,7 @@ for member in \
'./licenses/bootstrap-5.3.8/LICENSE' \
'./licenses/bootstrap-icons-1.13.1/LICENSE' \
'./licenses/jquery-4.0.0/LICENSE.txt' \
'./licenses/handlebars-4.7.9/LICENSE' \
'./licenses/handlebars-4.7.10/LICENSE' \
'./licenses/inherited-vendor/LICENSE-async-0.9.0' \
'./licenses/inherited-vendor/LICENSE-jquery-scrollintoview-1.8' \
'./licenses/inherited-vendor/LICENSE-jsonformatter-2009' \
Expand All @@ -182,10 +183,11 @@ for member in \
}
done

first_sha256=$(sha256sum "$artifact" | awk '{print $1}')
npm run test:product
cp "$artifact" "$first_archive"
bash scripts/build
cmp "$first_archive" "$artifact"
second_sha256=$(sha256sum "$artifact" | awk '{print $1}')
test "$first_sha256" = "$second_sha256"

echo "API_EXPLORER_REPRODUCIBLE_ARCHIVE_GUARD_OK source_maps=0 canonical_tar=1 deterministic_gzip=1 canonical_text_order=1 artifact_mode=0644 sha256=${second_sha256}"
echo "API_EXPLORER_RELEASE_GATES_OK version=${version} bootstrap_css=5.3.8 bootstrap_icons=1.13.1 bootstrap_javascript=0 modal=first-party dropdown=first-party"
Expand Down
48 changes: 48 additions & 0 deletions test/product/precompiled-runtime.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const {JSDOM} = require('jsdom');
const version = require('../../package.json').version;

for (const bundle of ['ui.js', 'ui.min.js']) {
for (const locale of ['en-US', 'zh-TW']) {
test(`${bundle} ${locale}: shipped runtime, partials and API escaping preserve the contract`, () => {
const dom = new JSDOM('', {runScripts: 'outside-only', url: 'https://api.example.test/v1'});
const window = dom.window;
window.autoInit = false;
window.pasturestackLocale = locale;
try {
window.eval(fs.readFileSync(path.join(__dirname, '../../dist', version, bundle), 'utf8'));
const runtime = window.Handlebars;
assert.equal(runtime.VERSION, '4.7.10');
assert.equal(runtime.compile, undefined);
assert.equal(runtime.precompile, undefined);
assert.equal(window.PastureStackI18n.getLocale(), locale);
assert.equal(window.document.documentElement.lang, locale);
window.document.body.innerHTML = runtime.templates['response.hbs']({});
assert.ok(window.document.body.textContent.includes(locale === 'zh-TW' ? '沒有可用的回應標頭。' : 'Response headers not available.'));

const remote = '<img src=x onerror="window.__xss=true"> API_ERROR_原文';
window.document.body.innerHTML = runtime.templates['response.hbs']({
res: {status: 403}, responseHeaders: [{name: 'X-Remote-Error', value: remote}]
});
assert.equal(window.document.querySelector('.response-header-value').textContent, remote);
assert.equal(window.document.querySelector('img'), null);
window.document.body.innerHTML = runtime.partials['field.hbs']({
type: 'string', formFieldName: 'name', value: remote, placeholder: '欄位 <name>', field: {}
});
const input = window.document.querySelector('input');
assert.equal(input.name, 'name');
assert.equal(input.value, remote);
assert.equal(input.placeholder, '欄位 <name>');
assert.equal(window.document.querySelector('img'), null);
assert.equal(window.__xss, undefined);
} finally {
dom.window.close();
}
});
}
}
Loading
Loading