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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,27 @@ jobs:
- run: pnpm check
- run: pnpm example
- run: pnpm test:demo
- run: pnpm test:comparison
- run: pnpm test:package
browsers:
name: Chromium / Firefox / WebKit
runs-on: ubuntu-latest
timeout-minutes: 20
# Match @playwright/test; browsers and OS dependencies are already installed.
# Avoid downloading apt packages from the runner's mirror on every run.
container:
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
options: --user 1001 --init --ipc=host
steps:
- 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
- name: Install pnpm for the container user
run: |
npm install -g --prefix "$RUNNER_TEMP/refmint-pnpm" pnpm@11.25.0
echo "$RUNNER_TEMP/refmint-pnpm/bin" >> "$GITHUB_PATH"
- run: pnpm install --frozen-lockfile --ignore-scripts
- run: pnpm build
- run: pnpm exec playwright install --with-deps chromium firefox webkit
- run: pnpm test:browser
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 0.1.0-alpha.2 — 2026-10-27

Second GitHub prerelease, including the existing-ID API, compatibility checks, comparison form and measured optimizations. Distributed as a package archive; not published to the npm registry.

- Optimize checksum evaluation and parsing without changing formats or diagnostics; use exact 50-bit chunks for bigint decoding. Add an optional `refmint/codec` import and strip implementation comments from shipped JS while preserving source/type documentation. Record full-root and codec-only sizes, a same-process comparison against the previous build, and differential behavior checks.
- Encode existing uint64 and UUID values with an explicit v1 contract, without allocating a second random ID. Preserve integer precision through bigint and reject overflow/noncanonical encodings.
- Keep alpha.1 random references unchanged and structurally separate from the new format; add frozen compatibility vectors and namespace/kind TypeScript brands.
- Add opt-in `diagnose` results with raw-input invalid-character positions and explicit length/namespace details. Existing parse result shapes remain unchanged; checksum errors never autocorrect a code.
- Add a return-lookup form and an independent Bech32m adapter with identical input rules, records and diagnostics. Record shared scenarios, mutation fixtures, adapter/loaded-code sizes, reference lengths and local timings with explicit evidence limits.

[Release notes and downloads](https://github.com/PerchAnton/refmint/releases/tag/v0.1.0-alpha.2).

## 0.1.0-alpha.1 — 2026-10-27

First GitHub prerelease, distributed as a package archive. The npm registry publication is separate. The API and reference format are experimental.
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ Run `pnpm install --frozen-lockfile --ignore-scripts` and `pnpm check`. Run `pnp
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.

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.

For the existing-ID format or comparison, run `pnpm compare` after the build and `pnpm test:comparison` to verify recorded source hashes and deterministic facts. Do not change frozen v1 vectors to accommodate an incompatible layout. The independent Bech32m adapter must not import RefMint helpers; keep both forms' inputs, validation contract and shared UI equivalent. Report library size and reference-length costs alongside any reduction in adapter code.

The build keeps JSDoc in declarations and removes comments only from emitted JS. Keep the full-root and codec-only size figures separate; include compression costs. After runtime changes, refresh the historical comparison with `pnpm compare:previous PATH_TO_PREVIOUS_DIST FULL_COMMIT_SHA` as described in [comparison methods](docs/comparison.md#before-and-after-optimization). The recorded previous baseline is `daaffe813fd0351752bd158314917267acddc1b6`. Do not compare historical timings collected on different machines as a before/after speedup.
49 changes: 41 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
[![CI](https://github.com/PerchAnton/refmint/actions/workflows/ci.yml/badge.svg)](https://github.com/PerchAnton/refmint/actions/workflows/ci.yml)
![Status: alpha](https://img.shields.io/badge/status-alpha-d9822b)
![Runtime dependencies: 0](https://img.shields.io/badge/runtime_dependencies-0-087d5d)
[![GitHub prerelease](https://img.shields.io/badge/release-0.1.0--alpha.1-087d5d)](https://github.com/PerchAnton/refmint/releases/tag/v0.1.0-alpha.1)
[![GitHub prerelease](https://img.shields.io/badge/release-0.1.0--alpha.2-087d5d)](https://github.com/PerchAnton/refmint/releases/tag/v0.1.0-alpha.2)

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.
RefMint is a small TypeScript library for public ticket, document and order references. It can generate random references or encode existing uint64/UUID values with a namespace 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. [Download the first GitHub prerelease](https://github.com/PerchAnton/refmint/releases/tag/v0.1.0-alpha.1) or build from source. The package uses ES modules and includes TypeScript declarations. Node.js 22+ and browsers with Web Crypto are the targets. This release is distributed through GitHub; it is not published to the npm registry.
**New in alpha.2:** preserve an existing uint64 or UUID with `createReferenceCodec`, inspect invalid input with `diagnose`, and load only the codec with `refmint/codec`. The release includes the optimized implementation and comparison evidence; the tagged source includes the complete comparison form. [Format v1](docs/format-v1.md) · [Comparison and measured trade-offs](docs/comparison.md).

**Alpha:** APIs may evolve. The existing-ID v1 wire contract is frozen; incompatible formats require a new version. [Download the current GitHub prerelease](https://github.com/PerchAnton/refmint/releases/tag/v0.1.0-alpha.2) or build from source. The package uses ES modules and includes TypeScript declarations. Node.js 22+ and browsers with Web Crypto are the targets. This release is distributed through GitHub; it is not published to the npm registry.

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

Expand All @@ -20,15 +22,15 @@ The image is generated from [recorded evidence](docs/evidence.json). See [measur
Install the versioned package archive into your application:

```sh
npm install --allow-remote=root https://github.com/PerchAnton/refmint/releases/download/v0.1.0-alpha.1/refmint-0.1.0-alpha.1.tgz
npm install --allow-remote=root https://github.com/PerchAnton/refmint/releases/download/v0.1.0-alpha.2/refmint-0.1.0-alpha.2.tgz
```

npm 12 requires explicit permission for URL dependencies. Pass `--allow-remote=root` on later `npm install` / `npm ci` runs for a project that keeps this URL dependency, too; the flag does not change global settings. Alternatively, download the `.tgz` asset and run `npm install ./refmint-0.1.0-alpha.1.tgz`. [npm's URL dependency setting](https://docs.npmjs.com/cli/v12/commands/npm-install/#allow-remote).
npm 12 requires explicit permission for URL dependencies. Pass `--allow-remote=root` on later `npm install` / `npm ci` runs for a project that keeps this URL dependency, too; the flag does not change global settings. Alternatively, download the `.tgz` asset and run `npm install ./refmint-0.1.0-alpha.2.tgz`. [npm's URL dependency setting](https://docs.npmjs.com/cli/v12/commands/npm-install/#allow-remote).

The release also includes `SHA256SUMS.txt` and the measured evidence. To run the local browser demo from the tagged source:
The release also includes `SHA256SUMS.txt`, verification metadata and all three measured-evidence reports. To run the local browser demo from the tagged source:

```sh
git clone --branch v0.1.0-alpha.1 https://github.com/PerchAnton/refmint.git
git clone --branch v0.1.0-alpha.2 https://github.com/PerchAnton/refmint.git
cd refmint
# Install pnpm 11.25.0 if it is not available.
npm install -g pnpm@11.25.0
Expand Down Expand Up @@ -86,6 +88,35 @@ If no budget is supplied, RefMint uses **26 random characters / 130 bits**. Choo

TypeScript also distinguishes `Reference<'ticket'>` from `Reference<'doc'>`. A plain string must pass the parser before it can be used as a branded reference. The brand is a compile-time aid, not an authorization boundary.

## Keep an existing ID

Available in alpha.2. It does not allocate another random ID or require an alias column:

```ts
import { createReferenceCodec } from 'refmint/codec'

const returns = createReferenceCodec({ prefix: 'rma', kind: 'uint64', version: 1 })
const printed = returns.format(returns.encode(123456n))
const result = returns.diagnose(printed.toUpperCase())
if (result.ok) {
console.log(result.value) // 123456n — query the original key, with authorization
} else {
console.log(result.error, result.details) // positions / expected / actual, when known
}
```

For UUIDs, use `kind: 'uuid'` with canonical lowercase UUID strings. A numeric codec requires `bigint` and preserves all 64 bits; it does not preserve leading zeros in a text identifier. Encoding is reversible and does not hide sequential numbers. [Full contract, diagnostics and compatibility](docs/format-v1.md).

The `refmint/codec` entry keeps the random generator and collision planner out of existing-ID-only forms. The same codec remains exported from `refmint`; both paths share its implementation and TypeScript brands. Both entry points are included in the alpha.2 archive; alpha.1 retains only the original random-reference API.

`createReferences(...).diagnose(input)` adds the same diagnostics for existing random references. Its `parse`/`parseCanonical` results and accepted formats remain unchanged. The new v1 grammar includes an underscore and version/kind symbols, so neither API mistakes the other format for one of its IDs.

Run `pnpm compare && pnpm demo`, then open **http://127.0.0.1:4173/comparison/**. Both forms use the same checksum and accept the same cases. The comparison records adapter code, total loaded JavaScript, reference lengths and local timings—including costs that favor the plain Bech32m solution. [Methods, source and limitations](docs/comparison.md). The extra checking does not make codes shorter; no human-entry advantage is established.

After optimization, the form loads **8,773 bytes** of RefMint JS plus adapter, versus **9,327 bytes** for Bech32m plus adapter. Summed per-file gzip still favors Bech32m: **3,353 vs 3,160 bytes**. The full RefMint root import is **14,147 bytes**, down from 15,255. These are unminified module totals, not bundle or install-archive sizes; RefMint's build now removes JS implementation comments while keeping source and declaration documentation.

A same-process Node 24 / Apple M3 run of the same 28 scenarios measured **1.78 million checks/s** after optimization, **0.56 million** before, and **0.82 million** for Bech32m plus adapter. This is a local diagnostic workload, not a general speed guarantee. All **123,146 differential comparisons** against the previous implementation matched. [Raw samples, source hashes and reproduction](docs/comparison.md#before-and-after-optimization).

## Scope and trade-offs

Use RefMint when **people enter a public reference** and your application needs consistent generation, display and validation rules. Keep a **UNIQUE database constraint**, handle collisions with bounded retries, and authorize every lookup. The checksum is public and can be recomputed by anyone: it is not a signature, password, invitation secret or proof of authenticity.
Expand All @@ -97,7 +128,7 @@ Use RefMint when **people enter a public reference** and your application needs
| A human-oriented alphabet and check digit across languages | [HardGuard25](https://github.com/snapsynapse/hardguard25) |
| A public-reference workflow with a declared collision budget and strict storage parsing | RefMint |

RefMint has six checksum characters of overhead and does not sort by time. It makes no speed advantage, adoption, reduced support cost or human-error-rate claim. Readable formatting and checksums already exist in other projects; this package's contribution is the focused API and tested workflow. Runtime code is independent of Nano ID. The checksum follows [BIP-173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) / [BIP-350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki), with attribution in [third-party notices](THIRD_PARTY_NOTICES.md).
RefMint has six checksum characters of overhead and does not sort by time. It makes no general speed advantage, adoption, reduced support cost or human-error-rate claim. Readable formatting and checksums already exist in other projects; this package's contribution is the focused API and tested workflow. Runtime code is independent of Nano ID. The checksum follows [BIP-173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) / [BIP-350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki), with attribution in [third-party notices](THIRD_PARTY_NOTICES.md).

## Verify and measure

Expand All @@ -106,6 +137,8 @@ pnpm check # build, standard vectors, independent oracle, mutati
pnpm example # actual lookup example
pnpm test:demo # malformed HTTP requests and the demo asset allowlist
pnpm evidence # regenerate measured evidence and the SVG above
pnpm compare # measure the existing-ID form against a Bech32m adapter
pnpm test:comparison # verify the recorded comparison facts and source hashes
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
Expand Down
13 changes: 8 additions & 5 deletions SPEC.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# RefMint alpha.1 format and API
# RefMint formats and API

This is a narrow application of generic Bech32m, not a new checksum algorithm or a Bitcoin address implementation. Freeze a deployed namespace's prefix and random length. Alpha formats may change in later versions; no transparent migration is promised.
This is a narrow application of generic Bech32m, not a new checksum algorithm or a Bitcoin address implementation. The random-reference format below preserves the alpha.1 contract. Freeze a deployed namespace's prefix and random length. The separate [existing-ID format v1](docs/format-v1.md) has an explicit version and a disjoint grammar; it never reinterprets random payloads as database IDs.

## Canonical representation

Expand All @@ -12,7 +12,7 @@ This is a narrow application of generic Bech32m, not a new checksum algorithm or
- Maximum canonical length: 87 ASCII characters, below Bech32m's 90-character limit.
- Random bytes: `crypto.getRandomValues`, one byte per payload symbol. Mask each byte by 31. Because 256 is divisible by 32, every symbol has eight preimages; this introduces no modulo bias for uniform bytes. No timestamp, counter, machine fingerprint, user-provided seed or insecure fallback.

The public API does not encode arbitrary bytes, accept custom alphabets or expose a general-purpose Bech32m codec. The internal codec is checked against all 21 generic BIP-350 test vectors and the independent `bech32` package in development tests.
The public API does not encode arbitrary bytes, accept custom alphabets or expose a general-purpose Bech32m codec. `createReferenceCodec` supports the two precisely defined existing-ID kinds in the v1 contract. The internal checksum codec is checked against all 21 generic BIP-350 test vectors and the independent `bech32` package in development tests.

## Length planning

Expand Down Expand Up @@ -48,10 +48,13 @@ Errors identify a validation stage, not necessarily the user's exact mistake. A

## Public exports

- `createReferences(options)` → immutable `{ prefix, plan, generate, parse, parseCanonical, format }`.
- `createReferences(options)` → immutable `{ prefix, plan, generate, parse, parseCanonical, diagnose, format }`.
- `planReferences(options)` → immutable plan without generating an ID or requiring randomness.
- `collisionProbability(randomLength, count)` → birthday approximation, with the same supported length/count limits.
- Types: `Reference`, `ReferenceOptions`, `ReferencePlan`, `References`, `ParseError`, `ParseResult`.
- Types: `Reference`, `ReferenceOptions`, `ReferencePlan`, `References`, `ParseError`, `ParseResult`, `ParseDiagnostic`, `ValidationDetails`.
- `createReferenceCodec(options)` and its types are defined in [format v1](docs/format-v1.md).

`diagnose(input)` follows the same rules as `parse(input)`, returning the same successful result or `{ ok: false, error, details }`. `parse` and `parseCanonical` retain their original result shapes. Diagnostics identify invalid character positions as zero-based UTF-16 offsets in the raw input, even with whitespace/grouping; non-BMP characters use their first code-unit offset. Length details carry `expected`, `actual` and an explicit `unit`; valid wrong namespaces report their expected/actual names. Checksum errors never guess positions or provide a replacement ID. Diagnostic fields are additional evidence about the failed validation stage, not proof of the user's intended code.

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.

Expand Down
Loading
Loading