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
17 changes: 12 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ on:
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
core:
name: Node ${{ matrix.node }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
Expand All @@ -19,23 +23,26 @@ jobs:
- { node: 24, os: windows-latest }
- { node: 24, os: macos-latest }
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: ${{ matrix.node }}
package-manager-cache: false
- run: npm install -g pnpm@11.25.0
- run: pnpm install --frozen-lockfile --ignore-scripts
- run: pnpm check
- run: pnpm example
- run: pnpm pack --pack-destination artifacts
- run: pnpm test:package
browsers:
name: Chromium / Firefox / WebKit
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: 24
package-manager-cache: false
- run: npm install -g pnpm@11.25.0
- run: pnpm install --frozen-lockfile --ignore-scripts
- run: pnpm build
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ dist/
*.tgz
playwright-report/
test-results/
artifacts/
.DS_Store
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@
- Parse human input, require canonical storage input, and format groups of four.
- Distinguish record namespaces in TypeScript and validate them at runtime.
- Include a ticket lookup demo, standard vectors, independent-oracle tests and reproducible evidence.
- Reject Unicode lookalikes before case folding, including the Kelvin sign that previously normalized to ASCII `k`.
- Snapshot configuration once so dynamic getters cannot make the prefix disagree with its length plan.
- Extend finite mutation tests with insertions, deletions, adjacent swaps and 3,000 distinct multiple-substitution samples; exercise Unicode rejection in three browser engines.
- Verify an offline installation of the packed archive, including public exports, TypeScript brands, documentation links and measured runtime hashes.
- Include the README graphic and supporting documents in the archive, and default future npm publications to the `alpha` tag.

This version number identifies the source/package candidate. Publication to npm or a GitHub release is not implied.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

Start with a concrete reproduction: what reference was entered, expected behavior, actual behavior, runtime and version. Use synthetic public references; do not post private records or access tokens.

Run `pnpm install --frozen-lockfile --ignore-scripts` and `pnpm check`. Changes to the form also need `pnpm test:browser` after installing Playwright's browser engines. Keep the format specification and examples consistent. Add a regression that demonstrates a real failure, and retain the standard vectors and independent oracle.
Run `pnpm install --frozen-lockfile --ignore-scripts` and `pnpm check`. Run `pnpm test:package` to verify the installed archive's API, types and documents. Changes to parsing or the form also need `pnpm test:browser` after installing Playwright's browser engines. Keep the format specification and examples consistent. Add a regression that demonstrates a real failure, and retain the standard vectors and independent oracle.

Use `refmint/<task>` for branches. Keep the runtime dependency-free and the API focused. New formats, frameworks or language ports need a demonstrated consumer need. Evidence must describe the environment and measurement method; no unsupported performance or adoption claims.

`pnpm evidence` regenerates recorded measurements and the README graphic. Do not replace another machine's measurements without labeling the new environment. Contributions to original code are under MIT OR Apache-2.0; preserve third-party notices.
After runtime changes, run `pnpm evidence` before `pnpm test:package`: the package check compares the installed JavaScript with the measured hashes. Builds use LF line endings on every platform. The evidence command regenerates recorded measurements and the README graphic and labels the new environment. Do not replace another machine's measurements without that label. Contributions to original code are under MIT OR Apache-2.0; preserve third-party notices.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

RefMint is a small TypeScript library for public ticket, document and order references. It combines a random payload, a type prefix and a standard Bech32m checksum. A form can distinguish a mistyped code from a valid reference for a different record type, before querying a database.

**Alpha:** the format and API may change. This repository is usable from source; no npm publication is claimed. Node.js 22+ and browsers with Web Crypto are the targets.
**Alpha:** the format and API may change. This repository is usable from source; no npm publication is claimed. The package uses ES modules and includes TypeScript declarations. Node.js 22+ and browsers with Web Crypto are the targets.

![Measured checks and the reference lifecycle](docs/assets/evidence.svg)

Expand Down Expand Up @@ -66,6 +66,7 @@ If no budget is supplied, RefMint uses **26 random characters / 130 bits**. Choo
| Canonical reference | Valid; unchanged storage key |
| Whole code in uppercase; exact groups of four | Valid; normalized to the same storage key |
| Mixed case, an unsupported character or broken grouping | Rejected |
| Unicode lookalikes such as `K` in place of `K` | `character`; never silently converted to ASCII |
| A payload or prefix change without recomputing the checksum | Rejected when the checksum detects the error |
| Valid reference generated for another prefix | `prefix` |
| Valid reference with a different payload length | `length` |
Expand Down Expand Up @@ -94,13 +95,16 @@ RefMint has six checksum characters of overhead and does not sort by time. It ma
pnpm check # build, standard vectors, independent oracle, mutation + type tests
pnpm example # actual lookup example
pnpm evidence # regenerate measured evidence and the SVG above
pnpm test:package # offline install of the .tgz, API + types + documentation checks
pnpm exec playwright install chromium firefox webkit
pnpm test:browser # actual form flow in three browser engines
pnpm pack --pack-destination artifacts
```

CI runs the core checks on Node 22, 24 and 26, including Windows and macOS, plus the browser example on Chromium, Firefox and WebKit. The workflow badge reports its current result; the matrix is a test target until that run succeeds. See [SPEC.md](SPEC.md), [CHANGELOG.md](CHANGELOG.md), [security policy](SECURITY.md) and [contribution guide](CONTRIBUTING.md).

The package check installs into a separate consumer with an empty dependency store and network access disabled for installation. It checks the exported API, namespace types, shipped documentation links and runtime hashes against the recorded evidence. Future npm alpha publications default to the `alpha` tag; that setting does not publish a package.

## License

Original code: **MIT OR Apache-2.0**, at your option. Standard checksum/reference attribution and separately licensed test vectors are recorded in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
4 changes: 3 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ For one code `n=1`, pairwise collision probability is zero and the planner still
`parse` accepts unknown input and returns a discriminated result; malformed input does not throw. Configuration errors and unavailable random sources do throw.

1. Non-string → `type`; raw input longer than 128 UTF-16 code units → `too_long` before further work.
2. Trim surrounding ASCII space, tab, CR and LF. Unicode whitespace is not silently removed.
2. Any non-ASCII character → `character`, before case folding. This includes Unicode whitespace and lookalikes such as `K`; they cannot normalize to an ASCII code. Trim surrounding ASCII space, tab, CR and LF.
3. Mixed upper/lowercase letters → `case`. Uniform uppercase is normalized to lowercase.
4. Find the last `1` and validate prefix syntax; malformed prefix/separator → `format`.
5. Ungrouped body is accepted. If hyphens occur, every group except the last must contain exactly four characters; last group must contain 1–4. Only these hyphens are removed; malformed groups → `format`.
Expand All @@ -55,6 +55,8 @@ Errors identify a validation stage, not necessarily the user's exact mistake. A

Options: `{ prefix }` for the 26-character default; `{ prefix, randomLength }` for a fixed length; or `{ prefix, expectedCount, maxCollisionProbability }` for planning. The two sizing modes cannot be combined. Unknown options are rejected so a misspelled budget cannot silently select a default. Different prefixes have different TypeScript brands; lengths within one prefix are enforced at runtime.

The factory reads each option once during construction. Its namespace and immutable plan use that same snapshot, even when an option is supplied by a getter. Later changes to the options object do not reconfigure an existing factory.

## Evidence limits

The Bech32m specification describes checksum properties. Our finite mutation tests exercise selected fixtures; they do not independently prove all error patterns. Formatting normalization intentionally accepts case and separator differences, so those are not claimed as detected mutations. A valid checksum does not imply unique issuance, a real record, an authorized user, resistance to forgery or better human performance. A unique storage constraint and an authorization policy remain application responsibilities.
2 changes: 1 addition & 1 deletion docs/assets/evidence.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 22 additions & 22 deletions docs/evidence.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"generated_at": "2026-10-27T08:32:02.185Z",
"generated_at": "2026-10-27T08:56:50.253Z",
"version": "0.1.0-alpha.1",
"environment": {
"node": "v24.19.0",
Expand Down Expand Up @@ -28,9 +28,9 @@
"files": [
{
"file": "dist/index.js",
"bytes": 6492,
"gzip_bytes": 2225,
"sha256": "5477d5db2026f04b8cbe8f85b406a5215ae8eef90c2d9d5ddd6193941184d222"
"bytes": 6814,
"gzip_bytes": 2358,
"sha256": "c039862817354759fc4133111e86ccad559540b23d5d2082bd3ad80c825aaf37"
},
{
"file": "dist/bech32m.js",
Expand All @@ -39,8 +39,8 @@
"sha256": "93d78425096f7dfb58c99c008c1256256e0a48d71d8d3b49a6f89a2bed3d62c6"
}
],
"total_bytes": 8850,
"summed_gzip_bytes": 3358
"total_bytes": 9172,
"summed_gzip_bytes": 3491
},
"timings": {
"method": "Single process, seven rounds after warmup; no competitor comparison. Generation includes Web Crypto and checksum; parsing includes checksum and normalization.",
Expand All @@ -49,30 +49,30 @@
"rounds": 7,
"warmup": 2000,
"ops_per_second": [
370603,
423272,
421309,
435002,
432156,
426264,
423388
373641,
420928,
417591,
420950,
419686,
417733,
420127
],
"median_ops_per_second": 423388
"median_ops_per_second": 419686
},
"parse_displayed": {
"iterations": 20000,
"rounds": 7,
"warmup": 2000,
"ops_per_second": [
479645,
533661,
548733,
540627,
550019,
535113,
535506
485884,
521867,
522076,
524402,
523370,
525297,
521497
],
"median_ops_per_second": 535506
"median_ops_per_second": 522076
}
},
"collision_model": [
Expand Down
6 changes: 4 additions & 2 deletions docs/evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ Run `pnpm check && pnpm evidence` from a clean checkout after installing the loc

Core tests check all 7 valid and 14 invalid generic Bech32m vectors from BIP-350. They compare generated output with a separate implementation (`bech32` 2.0.0) for all payload lengths from 8 through 64 and four prefixes. The planner is checked against exact BigInt pair counts at representative budgets. Type checks demonstrate namespace separation.

The evidence script enumerates 31 alternate symbols at every payload and checksum position for four known references. Their payload lengths are 8, 14, 26 and 64, giving `(14 + 20 + 32 + 70) × 31 = 4,216` substitutions. All must be rejected or the script fails. The core suite additionally checks 3,000 deterministic samples with 2–4 substituted positions. This is finite test evidence; it is not an exhaustive proof of the standard's error-detection properties. Case folding and valid grouping are deliberately accepted and are not counted as detected errors.
The evidence script enumerates 31 alternate symbols at every payload and checksum position for four known references. Their payload lengths are 8, 14, 26 and 64, giving `(14 + 20 + 32 + 70) × 31 = 4,216` substitutions. All must be rejected or the script fails. The core suite additionally checks 3,000 distinct deterministic samples with 2–4 substituted positions (1,000 per count), all single-symbol insertions and deletions in those four bodies, and adjacent swaps of unequal symbols. This is finite test evidence; it is not an exhaustive proof of the standard's error-detection properties. ASCII case folding and valid grouping are deliberately accepted and are not counted as detected errors. Non-ASCII input is rejected before case folding.

Browser tests exercise the actual form in Chromium, Firefox and WebKit: printed uppercase reference, one typo, unknown valid reference, replacing the sample ticket, and a narrow viewport. Hosted results are available in the CI workflow. Local results and hosted results must be identified separately.
Browser tests exercise the actual form in Chromium, Firefox and WebKit: printed uppercase reference, one typo, unknown valid reference, replacing the sample ticket, a narrow viewport and rejection of a Unicode lookalike. There are three tests per engine. Hosted results are available in the CI workflow. Local results and hosted results must be identified separately.

`pnpm test:package` packs the distributable and installs it offline into a separate consumer with an empty store. It checks runtime imports, TypeScript namespace brands, blocked private-module imports, shipped Markdown links and SHA-256 hashes of the measured JavaScript. The package includes the evidence image and documents; builds force LF line endings so the same source produces the same runtime hashes on the tested operating systems.

## Size and speed

Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"node": ">=22"
},
"sideEffects": false,
"publishConfig": { "access": "public", "tag": "alpha" },
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -30,8 +31,12 @@
},
"files": [
"dist",
"docs",
"README.md",
"SPEC.md",
"CHANGELOG.md",
"CONTRIBUTING.md",
"SECURITY.md",
"LICENSE-MIT",
"LICENSE-APACHE",
"THIRD_PARTY_NOTICES.md"
Expand All @@ -40,6 +45,7 @@
"build": "tsc -p tsconfig.json",
"test": "node --test tests/*.test.mjs",
"test:types": "tsc -p tests/tsconfig.json",
"test:package": "node scripts/check-package.mjs",
"check": "pnpm build && pnpm test && pnpm test:types",
"example": "node examples/ticket-lookup.mjs",
"evidence": "node scripts/evidence.mjs",
Expand Down
Loading
Loading