From fa782715db409200e0ed88b3b445e6aa2bfc486d Mon Sep 17 00:00:00 2001 From: PerchAnton <101743951+PerchAnton@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:59:35 +0200 Subject: [PATCH 1/2] Prepare stable release channels, support and exact archive checks --- CHANGELOG.md | 3 +- CONTRIBUTING.md | 17 +++---- README.md | 7 ++- SECURITY.md | 3 +- docs/evidence.json | 7 ++- docs/releasing.md | 89 ++++++++++++++++++++++++++++++++++ docs/stability.md | 6 ++- docs/stable-contract.md | 8 +-- docs/support.md | 52 ++++++++++++++++++++ package.json | 4 +- scripts/check-package.mjs | 39 ++++++++++----- scripts/release-channel.mjs | 20 ++++++++ tests/release-channel.test.mjs | 35 +++++++++++++ 13 files changed, 255 insertions(+), 35 deletions(-) create mode 100644 docs/releasing.md create mode 100644 docs/support.md create mode 100644 scripts/release-channel.mjs create mode 100644 tests/release-channel.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index df8945a..2e0e242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -## Unreleased +## 1.0.0 — unpublished candidate +- Prepare stable/latest and preview channel checks, exact-archive qualification and a best-effort support policy. Runtime and wire v1 remain unchanged; npm publication is a separate step. - Fix the public API contract for 1.0 preparation and document alpha.3 upgrade/rollback without changing wire v1. No stable version is published by this change. - Check the exact candidate archive against integrity-pinned npm alpha.3 and beta.2: all nine writer/reader pairs, historical and Billetsys vectors, errors, exports and strict TypeScript consumers. - Reject canonical-reader regressions for case/whitespace as well as grouping, and verify the documented exception classes across all three packages. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53a8368..a220469 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,15 +23,14 @@ Do not replace their pins or regenerate historical vectors to hide a break. Keep the TypeScript consumer and both writer/reader directions covered. Temporary consumers and archives are removed even after failures. -## Publishing a prerelease - -Use a clean, reviewed commit and verify that its CI passed. Keep the version, changelog and evidence version in sync. A published npm name/version cannot be reused. For the beta candidate, keep both `publishConfig.tag` and the explicit publication tag set to `beta`. Do not move the existing `alpha` or `latest` tags. - -1. Run `pnpm check`, `pnpm test:demo`, `pnpm test:comparison`, `pnpm test:workflows` and `pnpm test:package`; the matching CI must also cover browsers, PDF extraction/lookup and the supported Node/OS matrix. -2. Pack with `pnpm pack --pack-destination artifacts`. Record the commit, package SHA-256 and npm SHA-512 integrity. Inspect the file inventory and install this exact archive into an isolated consumer before publication. -3. Authenticate to `https://registry.npmjs.org` with `npm login --auth-type=web`. Enter passwords and second-factor challenges only in npm's login flow. Never commit credentials or put them in an issue/PR. Direct publication requires account 2FA or another authentication method supported by npm; prefer the interactive account flow for the first release. -4. Run `npm publish ./artifacts/refmint-VERSION.tgz --dry-run --ignore-scripts --tag beta --access public --registry=https://registry.npmjs.org`, substituting the actual version. Inspect the name, version, files and integrity. Then publish the same archive by removing only `--dry-run`. -5. Read the public registry metadata back. Check the version, `beta` tag and tarball integrity, then install `refmint@beta` in a fresh consumer and exercise both `refmint` and `refmint/codec`. Download the registry tarball and compare it with the published archive. Keep the proof with the release record. +## Preparing and publishing a release + +Follow [the release procedure](docs/releasing.md) and [support policy](docs/support.md). +Stable versions use `latest`; alpha uses `alpha`; beta/RC uses `beta`. Both +`publishConfig.tag` and the explicit publication tag must agree. The package gate +checks the channel. Use `pnpm test:package /absolute/path/to/candidate.tgz` to +qualify an existing final archive without rebuilding it. Publication is a separate +owner decision on a clean, reviewed commit with successful CI. The first npm alpha.2 archive has updated README, changelog and contributor documentation. All other files match the original GitHub alpha.2 archive; the original tag, download and checksums remain intact. Future releases should prepare registry instructions before creating either archive. diff --git a/README.md b/README.md index 36eab66..e80c9cf 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,11 @@ npm install --save-exact refmint@0.1.0-beta.2 [API compatibility and frozen v1 format](docs/beta-contract.md) · [Verified npm installation](docs/evidence/release-beta2.json). [API contract for 1.0 preparation](docs/stable-contract.md) · -[Upgrade checks from alpha.3](docs/migration-alpha3-to-1.md). The package remains in beta; -the compatibility matrix compares published releases with an unpublished archive. +[Upgrade checks from alpha.3](docs/migration-alpha3-to-1.md) · +[Release channels](docs/releasing.md) · [Support policy](docs/support.md). +This checkout prepares an **unpublished 1.0.0 candidate**; the installation above +still selects the published beta. The compatibility matrix compares both old +releases with the exact candidate archive. **Try an invoice lookup.** The [sample PDF](https://github.com/PerchAnton/refmint/blob/v0.1.0-beta.2/examples/workflows/sample-invoice.pdf) shows `INV-2026-000042`, the short alias `D1042` and the 21-character printed diff --git a/SECURITY.md b/SECURITY.md index dedb161..f362a96 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,4 +6,5 @@ Keep a unique database constraint, retry conflicts with a limit, authorize looku Do not attempt to repair an invalid code automatically: a guessed correction can select the wrong record. The parser only normalizes the presentation differences documented in SPEC.md. -For a potential vulnerability, use GitHub's private vulnerability reporting if available, or contact the repository owner through their GitHub profile without disclosing exploit details in a public issue. Ordinary reproducible bugs belong in issues. The current beta has not had an independent security audit. See the [beta scope](docs/beta-contract.md) and [stability criteria](docs/stability.md); a beta label is not a security assurance. +For a potential vulnerability, use GitHub's private vulnerability reporting if available, or contact the repository owner through their GitHub profile without disclosing exploit details in a public issue. Ordinary reproducible bugs belong in issues. The current package has not had an independent security audit. See the +[maintenance and support policy](docs/support.md). See the [beta scope](docs/beta-contract.md) and [stability criteria](docs/stability.md); a beta label is not a security assurance. diff --git a/docs/evidence.json b/docs/evidence.json index 306078a..ba6ad32 100644 --- a/docs/evidence.json +++ b/docs/evidence.json @@ -1,6 +1,6 @@ { "generated_at": "2026-10-27T13:21:13.955Z", - "version": "0.1.0-beta.2", + "version": "1.0.0", "environment": { "node": "v24.19.0", "platform": "darwin", @@ -114,5 +114,10 @@ "carried_forward": { "reason": "Runtime JavaScript bytes are unchanged; original alpha.2 measurement timestamps, environment and samples are retained.", "verified_date": "2026-11-05" + }, + "release_preparation": { + "previousVersion": "0.1.0-beta.2", + "runtimeUnchanged": true, + "note": "Version metadata carried forward for the unpublished 1.0.0 candidate; original measurement timestamps, environment and hashes are unchanged." } } diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..fdc532e --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,89 @@ +# Release channels and exact-archive qualification + +This branch prepares **1.0.0**. It does not publish it, move registry tags, or +approve a consumer dependency update. Use the published beta until a stable +release is explicitly announced. Package versions and wire v1 are independent. + +## Channels + +| Version | `publishConfig.tag` and explicit npm `--tag` | Intended use | +| --- | --- | --- | +| `X.Y.Z-alpha.N` | `alpha` | Experimental previews | +| `X.Y.Z-beta.N` or `X.Y.Z-rc.N` | `beta` | Compatibility/release trials | +| `X.Y.Z`, major at least 1 | `latest` | Reviewed stable releases | + +`latest` is npm's default install channel; there is no separate `stable` alias. +Never send a prerelease to `latest`, or a stable build to a preview tag. The +package gate checks this policy in both the checkout and installed archive. +It intentionally rejects unsupported prerelease labels, leading-zero numeric +identifiers and build metadata; this is our release policy, not a SemVer parser. +A dist-tag is mutable routing, not an integrity or compatibility guarantee. +Consumers should use an exact version and retain their lockfile. + +A beta publication changes only `beta`; a stable publication changes only +`latest`. Preserve `alpha` and the previous `beta` for explicit trials. Record +all registry tags immediately before and after publication. Never reassign a +published name/version or relabel an old beta archive as 1.0.0. See npm's +[publish](https://docs.npmjs.com/cli/v11/commands/npm-publish/) and +[dist-tag](https://docs.npmjs.com/cli/v11/commands/npm-dist-tag/) documentation. + +## Before publication + +The owner approves release separately from merging a preparation PR. No workflow +in this repository publishes automatically. Use a clean, reviewed commit whose +CI passed on that exact head, including Node/OS, browser and PDF jobs. + +1. Update version, channel, changelog and evidence version together. Keep old + measurement dates and environments when the runtime is unchanged; label the + carry-forward. Runtime changes require fresh evidence and compatibility checks. +2. Run `pnpm check`, `pnpm test:demo`, `pnpm test:comparison`, + `pnpm test:workflows` and `pnpm test:package`. Review known bugs/security reports, + the [support policy](support.md), [API contract](stable-contract.md), and + [migration checks](migration-alpha3-to-1.md). Preserve all frozen vectors. +3. Pack once into a directory outside the checkout: + + ```sh + pnpm pack --pack-destination /absolute/path/to/release-artifacts + pnpm test:package /absolute/path/to/release-artifacts/refmint-1.0.0.tgz + ``` + + The second command does **not** rebuild or repack. It installs that archive + offline, checks metadata/channel, inventory, licenses, documentation, runtime + hashes, types and the alpha.3/beta.2/candidate matrix, then prints SHA-256 and + npm SHA-512 integrity. It checks the archive did not change during validation. + Temporary consumers/stores are removed on success or failure. Keep the exact + qualified archive for publication; if its bytes or source change, qualify again. +4. Record commit, CI URLs, commands/results, platform versions, package inventory, + SHA-256 and SHA-512 integrity in the release record. The file hash identifies + the candidate; a record containing its own archive hash must live outside it. +5. Authenticate to `https://registry.npmjs.org` using npm's web login/2FA flow. + Enter secrets only there. Trusted publishing requires a separately configured + and reviewed workflow; a repository URL does not grant publish permission. +6. Preview the **same archive** with the explicit channel: + + ```sh + npm publish /absolute/path/to/release-artifacts/refmint-1.0.0.tgz --dry-run --ignore-scripts --tag latest --access public --registry=https://registry.npmjs.org + ``` + + Check name, version, inventory and integrity against step 4. For previews use + their exact filename and `--tag alpha` or `--tag beta` from the table. A dry run + does not prove authentication, registry acceptance or successful publication. + +## Publication and rollback + +After explicit release approval, publish the qualified archive with the same +command, removing only `--dry-run`. Read registry metadata back, verify the chosen +tag/version and unchanged other tags, download the public tarball and compare its +integrity and bytes. Install that registry archive in a fresh consumer and run +`pnpm test:package /absolute/path/to/downloaded.tgz` from the same checkout. +Attach the qualified archive, hashes and evidence to the matching GitHub release. +Do not claim publication until registry readback succeeds. + +For a bad release, stop rollout and communicate the affected versions. Publish a +new version for a fix; never overwrite an existing one. Deprecation or moving +`latest` back requires an explicit owner decision and preserved evidence. A tag +rollback does not change existing lockfiles or already-installed applications. +Application owners decide when to revert their exact pin and deploy the old build. +Retain the v1 reader/configuration so newly issued codes still work after rollback; +see the [migration guide](migration-alpha3-to-1.md). Stable release approval does +not approve Billetsys deployment, permissions, or enabling its feature flag. diff --git a/docs/stability.md b/docs/stability.md index b0a16b9..9401d32 100644 --- a/docs/stability.md +++ b/docs/stability.md @@ -1,4 +1,4 @@ -# Beta scope and stability criteria +# Stability criteria The [published beta](https://www.npmjs.com/package/refmint/v/0.1.0-beta.1) defines a [compatibility contract](beta-contract.md). Pin the exact version for a trial @@ -17,7 +17,9 @@ The [1.0 API contract](stable-contract.md) fixes the surface being prepared. `pnpm test:package` now compares its exact archive with integrity-pinned alpha.3 and beta.2 in both directions, including TypeScript consumers and historical Billetsys codes. See [upgrade and rollback checks](migration-alpha3-to-1.md). -This work does not publish 1.0 or complete the remaining gates below. +The 1.0.0 candidate also defines [support](support.md) and [release channels](releasing.md), +including qualification of an existing final archive. It is not yet published; +final acceptance and consumer rollout remain separate. ## Criteria before a stable release diff --git a/docs/stable-contract.md b/docs/stable-contract.md index ff6242c..502ec22 100644 --- a/docs/stable-contract.md +++ b/docs/stable-contract.md @@ -1,7 +1,7 @@ # Public API contract for 1.0 preparation This contract fixes the public surface being prepared for RefMint 1.0. The -repository still builds an unpublished candidate with the current beta version; +repository builds an unpublished 1.0.0 candidate; this document does not publish 1.0, move npm tags, or authorize a consumer upgrade. Changes under preparation must preserve this contract and the historical-release checks. Release and support readiness remain separate [gates](stability.md). @@ -125,8 +125,8 @@ A necessary security correction needs a documented impact analysis, regression and migration or repair path; never conceal a compatibility failure by replacing old fixtures. Follow [SECURITY.md](../SECURITY.md) for disclosure. This contract does not promise an independent audit, calendar support period or response-time -SLA. Those release/maintenance decisions remain explicit gates before stable -publication. Neither a stable API nor its checksum proves authorization, record +SLA. The [support policy](support.md) and [release procedure](releasing.md) +apply before stable publication. Neither a stable API nor its checksum proves authorization, record existence, or a practical manual-entry advantage. ## Executable checks @@ -153,7 +153,7 @@ pnpm test:compatibility /absolute/path/to/refmint-candidate.tgz ``` The candidate's version must match the checkout; it is identified by its archive -hash even while its manifest still says beta.2. Historical digests and vectors +hash and its candidate manifest version. Historical digests and vectors must not be regenerated to accommodate changes. The matrix supplements, rather than replaces, independent Bech32m, boundary, browser, PDF and application tests. See the [alpha.3 migration guide](migration-alpha3-to-1.md). diff --git a/docs/support.md b/docs/support.md new file mode 100644 index 0000000..efc5080 --- /dev/null +++ b/docs/support.md @@ -0,0 +1,52 @@ +# Maintenance and support policy + +This policy is prepared for the first stable 1.x release. The 1.0.0 candidate is +not yet published. RefMint is maintained by PerchAnton on a best-effort basis; +there is no paid support, response-time SLA, promised release date or independent +security audit. Stable describes a compatibility commitment, not a security or +human-performance guarantee. + +## Supported lines + +- After 1.0 is published, fixes target the latest stable 1.x release. Older 1.x + minors/patches are not separate maintained branches; update to the latest + compatible patch/minor after application testing. +- Alpha/beta/RC versions are previews, not long-term support lines. Historical + alpha.3/beta.2 archives and vectors remain compatibility baselines. Keeping + previously issued codes readable does not promise backports to old packages. +- A future major must announce its migration/support plan before publication. + Ending 1.x maintenance requires an explicit notice with the supported successor + and migration path. No end date is assigned here, and no indefinite staffing + or vulnerability-response guarantee is implied. + +## Compatibility commitments + +Keep the [public API contract](stable-contract.md) and [wire v1](format-v1.md). +Incompatible changes to public types, errors, accepted values, defaults or minimum +platform requirements require a major and migration notes. Additive changes must +also preserve exhaustive TypeScript consumers; an added union case is not +necessarily compatible. Deprecation must be documented with an alternative and +cannot remove working 1.x behavior in a minor or patch. + +Old v1 and legacy random references must keep their identity and supported +normalization. Applications retain namespace/kind/version and random payload +length, and keep necessary readers for the lifetime of their issued references. +Do not replace fixtures to hide a failure. A security correction that affects +compatibility needs a regression, explicit impact analysis and a coordinated +repair/migration path, never silent reinterpretation as another record. + +## Environments and reports + +The supported baseline is Node 22+, native BigInt/ES2022 and ESM; secure Web Crypto +is required for random generation only. CI tests Node 22/24/26 on Linux, Node 24 +on Windows/macOS, and the pinned Chromium/Firefox/WebKit engines. TypeScript is +checked with 5.9.3 in strict NodeNext. This is not every OS/runtime/browser version. +Removing a promised minimum environment is a breaking change. New combinations +need reproducible evidence before being advertised as tested. + +For ordinary bugs, open an issue with exact package version, environment, +synthetic input, expected/actual results and a minimal reproduction. Keep private +records, credentials and account data out of reports. Report security concerns +through [SECURITY.md](../SECURITY.md). Consumer maintainers own authorization, +lookup/rate limits, uniqueness, rollout and rollback. Library tests cannot certify +those controls in a deployed application. See the [adoption guide](adoption.md). diff --git a/package.json b/package.json index f22890d..65237fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "refmint", - "version": "0.1.0-beta.2", + "version": "1.0.0", "description": "Generate public references or encode existing IDs with strict input diagnostics and a Bech32m checksum.", "type": "module", "license": "MIT OR Apache-2.0", @@ -24,7 +24,7 @@ "sideEffects": false, "publishConfig": { "access": "public", - "tag": "beta" + "tag": "latest" }, "exports": { ".": { diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs index a5fe276..160db3c 100644 --- a/scripts/check-package.mjs +++ b/scripts/check-package.mjs @@ -1,5 +1,6 @@ // Exercise the distributable, rather than importing from this checkout's dist/. import assert from 'node:assert/strict'; +import { assertReleaseChannel } from './release-channel.mjs'; import { mkdtemp, readFile, readdir, mkdir, writeFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, dirname, resolve } from 'node:path'; @@ -10,6 +11,7 @@ import { createHash } from 'node:crypto'; const root = fileURLToPath(new URL('..', import.meta.url)); const manager = process.env.npm_execpath; assert.ok(manager, 'Run this check with pnpm test:package'); +assert.ok(process.argv.length <= 3, 'Usage: pnpm test:package [candidate.tgz]'); const temp = await mkdtemp(join(tmpdir(), 'refmint-package-')); let stale; function run(script, args, cwd) { @@ -19,28 +21,37 @@ function run(script, args, cwd) { } try { const source = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')); - const packs = join(temp, 'packs'); - await mkdir(packs); - // A previous build may have emitted a module that has since been deleted. - await mkdir(join(root, 'dist'), { recursive: true }); - stale = await mkdtemp(join(root, 'dist', 'refmint-stale-')); - await writeFile(join(stale, 'removed-module.js'), '// Synthetic old build output.\n'); - run(manager, ['pack', '--pack-destination', packs], root); - await assert.rejects(readFile(join(stale, 'removed-module.js')), { code: 'ENOENT' }); - const archives = (await readdir(packs)).filter(file => file.endsWith('.tgz')); - assert.equal(archives.length, 1); + assertReleaseChannel(source); + let archive = process.argv[2] && resolve(process.argv[2]); + if (!archive) { + const packs = join(temp, 'packs'); + await mkdir(packs); + // A previous build may have emitted a module that has since been deleted. + await mkdir(join(root, 'dist'), { recursive: true }); + stale = await mkdtemp(join(root, 'dist', 'refmint-stale-')); + await writeFile(join(stale, 'removed-module.js'), '// Synthetic old build output.\n'); + run(manager, ['pack', '--pack-destination', packs], root); + await assert.rejects(readFile(join(stale, 'removed-module.js')), { code: 'ENOENT' }); + const archives = (await readdir(packs)).filter(file => file.endsWith('.tgz')); + assert.equal(archives.length, 1); + archive = join(packs, archives[0]); + } + const bytes = await readFile(archive); + const sha256 = createHash('sha256').update(bytes).digest('hex'); + const integrity = 'sha512-' + createHash('sha512').update(bytes).digest('base64'); const consumer = join(temp, 'consumer'); await mkdir(consumer); await writeFile(join(consumer, 'package.json'), JSON.stringify({ name: 'refmint-package-check', private: true, type: 'module', packageManager: source.packageManager, - dependencies: { refmint: 'file:' + join(packs, archives[0]).replaceAll('\\', '/') } + dependencies: { refmint: 'file:' + archive.replaceAll('\\', '/') } })); // Offline + a fresh store: any accidental runtime dependency fails this check. run(manager, ['install', '--offline', '--ignore-scripts', '--no-frozen-lockfile', '--store-dir', join(temp, 'store')], consumer); const installed = join(consumer, 'node_modules', 'refmint'); const packaged = JSON.parse(await readFile(join(installed, 'package.json'), 'utf8')); assert.equal(packaged.version, source.version); - assert.equal(packaged.publishConfig.tag, 'beta'); + assertReleaseChannel(packaged); + assert.deepEqual(packaged.publishConfig, source.publishConfig); assert.deepEqual(packaged.dependencies ?? {}, {}); const files = await readdir(installed, { recursive: true }); assert.deepEqual((await readdir(join(installed, 'dist'))).sort(), @@ -133,7 +144,9 @@ if (decoded.ok) { const original: bigint = decoded.value; void original; } `); run(join(root, 'node_modules/typescript/bin/tsc'), ['--noEmit', '--strict', '--target', 'ES2022', '--module', 'NodeNext', '--moduleResolution', 'NodeNext', 'types.mts'], consumer); // Reuse this exact archive for the historical-release matrix on every core CI platform. - console.log(run(join(root, 'scripts/check-compatibility.mjs'), [join(packs, archives[0])], root)); + console.log(run(join(root, 'scripts/check-compatibility.mjs'), [archive], root)); + assert.equal(createHash('sha256').update(await readFile(archive)).digest('hex'), sha256, 'Archive changed during verification'); + console.log('Verified package: ' + JSON.stringify({ version: source.version, tag: packaged.publishConfig.tag, sha256, integrity })); console.log(`PASS: ${source.name}@${source.version} archive installs offline; ESM, types, documentation and licenses are intact.`); } finally { if (stale) await rm(stale, { recursive: true, force: true }); diff --git a/scripts/release-channel.mjs b/scripts/release-channel.mjs new file mode 100644 index 0000000..0862d27 --- /dev/null +++ b/scripts/release-channel.mjs @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; + +// Release policy is intentionally narrower than arbitrary valid SemVer: +// alpha/beta/rc.N previews, and stable majors starting at 1. No build aliases. +export function releaseTag(version) { + assert.equal(typeof version, 'string', 'Version must be a string'); + const number = '(?:0|[1-9][0-9]*)'; + const match = new RegExp(`^(${number})\\.${number}\\.${number}(?:-(alpha|beta|rc)\\.(${number}))?$`).exec(version); + assert.ok(match && match[0] === version, 'Use X.Y.Z or X.Y.Z-alpha/beta/rc.N without build metadata'); + if (match[2]) return match[2] === 'alpha' ? 'alpha' : 'beta'; + assert.notEqual(match[1], '0', 'Stable releases start at major 1'); + return 'latest'; +} + +export function assertReleaseChannel(manifest) { + assert.equal(manifest.name, 'refmint'); + assert.equal(manifest.publishConfig?.access, 'public'); + assert.equal(manifest.publishConfig?.tag, releaseTag(manifest.version), + 'The publication tag must match the version channel'); +} diff --git a/tests/release-channel.test.mjs b/tests/release-channel.test.mjs new file mode 100644 index 0000000..763db55 --- /dev/null +++ b/tests/release-channel.test.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { assertReleaseChannel, releaseTag } from '../scripts/release-channel.mjs'; + +test('stable majors use latest; previews never use latest', () => { + for (const [version, tag] of [ + ['1.0.0', 'latest'], ['1.0.1', 'latest'], ['2.0.0', 'latest'], + ['0.1.0-alpha.3', 'alpha'], ['0.1.0-beta.2', 'beta'], + ['1.0.0-rc.1', 'beta'], ['1.1.0-beta.0', 'beta'] + ]) { + assert.equal(releaseTag(version), tag); + assertReleaseChannel({ name: 'refmint', version, publishConfig: { access: 'public', tag } }); + for (const wrong of ['latest', 'alpha', 'beta', 'stable'].filter(value => value !== tag)) { + assert.throws(() => assertReleaseChannel({ name: 'refmint', version, + publishConfig: { access: 'public', tag: wrong } })); + } + } +}); + +test('release policy rejects ambiguous, malformed and unsupported versions', () => { + for (const version of [undefined, null, 1, '', '0.1.0', 'v1.0.0', '01.0.0', + '1.00.0', '1.0.01', '1.0.0-beta', '1.0.0-beta.01', '1.0.0-dev.1', + '1.0.0+build', '1.0.0-rc.1+build', '1.0.0\n', ' 1.0.0']) { + assert.throws(() => releaseTag(version), String(version)); + } +}); + +test('manifest identity and explicit public channel are required', () => { + const valid = { name: 'refmint', version: '1.0.0', publishConfig: { access: 'public', tag: 'latest' } }; + for (const invalid of [ + { ...valid, name: 'another-package' }, { ...valid, publishConfig: undefined }, + { ...valid, publishConfig: { access: 'public' } }, + { ...valid, publishConfig: { access: 'restricted', tag: 'latest' } } + ]) assert.throws(() => assertReleaseChannel(invalid)); +}); From 9ec884403a97e00074716d15a359b687ed7dd884 Mon Sep 17 00:00:00 2001 From: PerchAnton <101743951+PerchAnton@users.noreply.github.com> Date: Sat, 7 Nov 2026 18:05:15 +0200 Subject: [PATCH 2/2] Check supplied archive evidence against the reviewed source --- docs/releasing.md | 3 ++- scripts/check-package.mjs | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/releasing.md b/docs/releasing.md index fdc532e..acc4491 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -49,7 +49,8 @@ CI passed on that exact head, including Node/OS, browser and PDF jobs. The second command does **not** rebuild or repack. It installs that archive offline, checks metadata/channel, inventory, licenses, documentation, runtime - hashes, types and the alpha.3/beta.2/candidate matrix, then prints SHA-256 and + hashes against the reviewed checkout evidence, types and the + alpha.3/beta.2/candidate matrix, then prints SHA-256 and npm SHA-512 integrity. It checks the archive did not change during validation. Temporary consumers/stores are removed on success or failure. Keep the exact qualified archive for publication; if its bytes or source change, qualify again. diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs index 160db3c..4e41157 100644 --- a/scripts/check-package.mjs +++ b/scripts/check-package.mjs @@ -63,6 +63,9 @@ try { assert.match(await readFile(join(installed, 'dist/index.d.ts'), 'utf8'), /A validated canonical reference/, 'Retain declaration documentation'); const evidence = JSON.parse(await readFile(join(installed, 'docs/evidence.json'), 'utf8')); assert.equal(evidence.version, source.version, 'Refresh evidence for this version'); + // A supplied archive must not certify modified runtime bytes with its own edited hashes. + assert.deepEqual(evidence, JSON.parse(await readFile(join(root, 'docs/evidence.json'), 'utf8')), + 'Archive evidence must match the reviewed checkout'); assert.deepEqual(evidence.size.files.map(file => file.file).sort(), (await readdir(join(installed, 'dist'))).filter(file => file.endsWith('.js')).map(file => 'dist/' + file).sort()); for (const file of evidence.size.files) { const bytes = await readFile(join(installed, file.file));