S00-019: Validate browser SCRAM-SHA-512 through public mechanism composition

Changes: Add an isolated pinned mechanism wrapper using Stanza's public constructor and factory APIs, preserve the SCRAM-SHA-512 wire name and fail-closed final guard, and document the proposed ADR, browser bundle evidence, independent Scramp vectors, notices and limits. Leave parent selection, dependency experiments, specs and shared contracts unchanged.
Validation: Fresh-cache npm ci and Vite browser build pass; 87 focused tests pass, including RFC HMAC vectors, original defect reproduction, independent full SHA-512 exchanges and rejected invalid finals/order/reset paths. Hashed Scramp oracle fixture comparison passes. Dependency check verifies 49 locked entries, 25 installed pins/licenses and 340 unmodified Stanza files. make check passes 72 tooling tests. Native browsers, WSS/live server interoperability and production readiness are untested; independent security review remains pending.

Task: S00-019
Agent: auto-crypto-c68801170fa4
Role: crypto
Action: claim
Status: review

Task title: Validate browser-compatible SCRAM-SHA-512 through a public mechanism API
This commit is contained in:
wt committed 2026-10-15 02:48:39 +07:00
1 parent e69372c6db
commit 07c9967d02
26 files changed
+6399 -8

No files matched your search

@@ -0,0 +1,3 @@
engine-strict=true
strict-ssl=true
registry=https://registry.npmjs.org/
@@ -0,0 +1,55 @@
# Locked dependency license inventory
All optional build-host entries are included; installed graph checked separately.
| Package | Version | License |
| --- | --- | --- |
| @oxc-project/types | 0.154.0 | MIT |
| @rolldown/binding-android-arm-eabi | 1.2.14 | MIT |
| @rolldown/binding-android-arm64 | 1.2.14 | MIT |
| @rolldown/binding-darwin-arm64 | 1.2.14 | MIT |
| @rolldown/binding-darwin-x64 | 1.2.14 | MIT |
| @rolldown/binding-freebsd-x64 | 1.2.14 | MIT |
| @rolldown/binding-linux-arm-gnueabihf | 1.2.14 | MIT |
| @rolldown/binding-linux-arm64-gnu | 1.2.14 | MIT |
| @rolldown/binding-linux-arm64-musl | 1.2.14 | MIT |
| @rolldown/binding-linux-ppc64-gnu | 1.2.14 | MIT |
| @rolldown/binding-linux-s390x-gnu | 1.2.14 | MIT |
| @rolldown/binding-linux-x64-gnu | 1.2.14 | MIT |
| @rolldown/binding-linux-x64-musl | 1.2.14 | MIT |
| @rolldown/binding-openharmony-arm64 | 1.2.14 | MIT |
| @rolldown/binding-win32-arm64-msvc | 1.2.14 | MIT |
| @rolldown/binding-win32-x64-msvc | 1.2.14 | MIT |
| @rolldown/pluginutils | 1.0.1 | MIT |
| @types/node | 22.20.5 | MIT |
| @types/punycode | 2.1.4 | MIT |
| @types/ws | 8.18.2 | MIT |
| detect-libc | 2.1.2 | Apache-2.0 |
| events | 3.3.0 | MIT |
| fdir | 6.5.0 | MIT |
| fsevents | 2.3.3 | MIT |
| lightningcss | 1.33.0 | MPL-2.0 |
| lightningcss-android-arm64 | 1.33.0 | MPL-2.0 |
| lightningcss-darwin-arm64 | 1.33.0 | MPL-2.0 |
| lightningcss-darwin-x64 | 1.33.0 | MPL-2.0 |
| lightningcss-freebsd-x64 | 1.33.0 | MPL-2.0 |
| lightningcss-linux-arm-gnueabihf | 1.33.0 | MPL-2.0 |
| lightningcss-linux-arm64-gnu | 1.33.0 | MPL-2.0 |
| lightningcss-linux-arm64-musl | 1.33.0 | MPL-2.0 |
| lightningcss-linux-x64-gnu | 1.33.0 | MPL-2.0 |
| lightningcss-linux-x64-musl | 1.33.0 | MPL-2.0 |
| lightningcss-win32-arm64-msvc | 1.33.0 | MPL-2.0 |
| lightningcss-win32-x64-msvc | 1.33.0 | MPL-2.0 |
| nanoid | 3.3.20 | MIT |
| picocolors | 1.1.1 | ISC |
| picomatch | 4.0.7 | MIT |
| postcss | 8.5.30 | MIT |
| punycode | 2.3.1 | MIT |
| rolldown | 1.2.14 | MIT |
| sdp | 3.2.2 | MIT |
| source-map-js | 1.2.2 | BSD-3-Clause |
| stanza | 12.22.1 | MIT |
| tinyglobby | 0.2.17 | MIT |
| undici-types | 6.21.0 | MIT |
| vite | 8.3.4 | MIT |
| ws | 8.22.0 | MIT |
@@ -0,0 +1,47 @@
# Independent oracle licenses
Test-only Python dependencies; not browser/app/runtime dependencies.
## scramp 1.4.6
MIT No Attribution
Copyright The Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## asn1crypto 1.5.1
Copyright (c) 2015-2022 Will Bond <will@wbond.net>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,103 @@
# Isolated browser SCRAM-SHA-512 validation
S00-019 fixture experiment, proposed for independent security review. No live
account/transport or parent library selection. Node executes the **browser-built
backend**, not Stanza's Node crypto. This is browser bundle/backend evidence,
not an actual Chrome/Firefox/Safari execution or WSS interoperability result.
## Reproduce
Use accepted Node 24.21.0 / npm 11.19.0. Existing reviewed macOS ARM64 runtime:
```sh
export PATH=/private/tmp/luma-stack-runtime/node-v24.21.0-darwin-arm64/bin:$PATH
npm ci --prefix experiments/0002-xmpp-library/sha512-validation --ignore-scripts --no-audit --no-fund --cache /private/tmp/luma-sha512-review-cache --fetch-retries=0 --fetch-timeout=15000
npm test --prefix experiments/0002-xmpp-library/sha512-validation
python3 experiments/0002-xmpp-library/sha512-validation/check-dependencies.py
```
Use a new empty cache for independent clean-install evidence. The dependency
check also reads the original installed candidate at the parent node_modules
for byte comparison; it does not modify it. If unavailable, reproduce its pinned
install under the parent task's authority or compare Stanza against its locked
tarball separately; do not change the parent from this task.
`npm test` builds before running 87 checks. Vite's default browser resolution
selects stanza/platform/browser. events 3.3.0 is the explicit browser EventEmitter
polyfill. Build fails if module IDs show Node backend/builtins/browser externals
or no browser HMAC. Output `dist/modules.json` lists resolved modules;
`dist/browser.mjs` is a self-contained ESM bundle. Backend tests inspect graph,
no runtime imports/require, and exported platform name `browser`. All integrated
exchanges import this artifact, never Node source. Node crypto is an independent
primitive oracle in tests only; the integration suite has no Node crypto import.
The native RNG path uses host getRandomValues, not a fabricated success value.
## Independent full-exchange oracle
The checked-in `test/scramp-vectors.json` is fixed output from **Scramp 1.4.6**
client and server (Python test-only MIT-0 dependency, asn1crypto 1.5.1 MIT).
Both client proof and server-final are independently verified. Inputs combine
public RFC 7677 nonce/salt/4096-iteration data with SHA-512 and synthetic passwords
pencil, 131 A characters and péncil. These SHA-512 transcripts are independently
derived vectors, not a claim that RFC 7677 specifies SHA-512 examples.
```sh
python3 -m venv /private/tmp/luma-sha512-review-oracle
/private/tmp/luma-sha512-review-oracle/bin/python -m pip install --require-hashes --only-binary=:all: --no-cache-dir -r experiments/0002-xmpp-library/sha512-validation/oracle-requirements.txt
/private/tmp/luma-sha512-review-oracle/bin/python experiments/0002-xmpp-library/sha512-validation/oracle.py
```
Python >=3.8; each wheel version/hash is exact. Default oracle mode compares
existing fixed fixtures; it does not rewrite them. `--write` is an explicit
fixture-generation operation. Scramp is excluded from the app/browser graph.
1.4.6 supports the available Python 3.9 host and the documented APIs; no current
release or supply-chain/advisory clearance is inferred. Notices are preserved.
## Public mechanism path
`Sha512Mechanism` composes Stanza's exported `SCRAM` using its public string
constructor with `SCRAM-SHA512`, selecting the existing documented-by-shipped-code
`sha512` alias. Its separate Mechanism.name is always **SCRAM-SHA-512**. Internal
alias never goes on the wire. Public Factory.disable/register installs that
constructor. No Stanza private field is read/changed, no subclass/private-method
hook or vendor/bundler crypto patch. Actual cryptographic primitives, PBKDF-style
SCRAM composition, SASLprep and verifier stay in unmodified Stanza library code.
This is version-specific public API composition, not an upstream endorsement of
a future stable alias contract; pins and full vectors are mandatory safeguards.
The isolated feature handler is copied from reviewed S00-017, narrowed to SHA-512
and 88-character base64 verifier, and registers the new mechanism. Install via
Client.use on a fresh Client before any negotiation; never install after stock
SASL begins. Mechanism priorities filter to SHA-512 even alongside PLUS/256/1/PLAIN
offers; unsupported-only offers fail. Complete product fallback policy stays
with S00-003. No real channel binding exists; tlsUnique input fails rather than
creating PLUS or a fabricated binding. Initial GS2 is n,, and c=biws.
Guard requires completed proof send before success, strict bounded first/final
syntax, then real processSuccess/finalize with both authenticated flags. Missing,
forged, malformed, premature/duplicate/racing final and replay fail without auth
or restart. Async reset/send failures discard continuation. No derived cache is
persisted/emitted and no raw credentials/peer data is logged by authored code.
Captured send/transport/disconnect effects are explicit fixtures; actual Client,
registry/XML parser, feature dispatcher, mechanism and crypto run.
## Limits
Only server-final in success additional-data, no SCRAM extensions/separate final
challenge. Limits 4096 decoded bytes and 100000 iterations are fixture policy.
Synchronous library PBKDF work can block the UI; no worker/performance acceptance.
No constant-time claim: Stanza compares verifier strings. No timeout is added;
server silence stays unauthenticated until external reset/cancellation. No
arbitrary Unicode/SASLprep conformance certification beyond the fixed case.
Native browser engine/RNG execution, WSS/TLS, independent live server/client,
parent bounded-ingress integration, complete mechanism policy and release
readiness remain outside this experiment. No TLS/certificate/CORS bypass or
plaintext downgrade. Browser syntax floors match accepted Vite targets
Chrome/Edge 111, Firefox 114, Safari 16.4; API requirements include crypto RNG,
fetch and WebSocket. These are build floors, not a certified product matrix.
Licenses: full locked graph in LICENSE_INVENTORY.md, installed notices including
Stanza browser crypto ancestry in THIRD_PARTY_NOTICES.md; oracle notices separate.
Vite/build native components are not browser runtime crypto. events maintenance
has few contributors per upstream; Stanza crypto is reviewed here with vectors,
not newly audited. Dependency/advisory/provenance review remains release work.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,10 @@
import client from 'stanza/Client.js';
import jxt from 'stanza/jxt/index.js';
import platform from 'stanza/platform';
import sasl from 'stanza/lib/sasl/index.js';
export {default as validatedSasl} from './validated-sasl.mjs';
export {Sha512Mechanism} from './sha512-mechanism.mjs';
export const Client = client.default;
export const parse = jxt.parse;
export const {Buffer, createHash, createHmac, name: backend, randomBytes} = platform;
export const StockScram = sasl.SCRAM;
@@ -0,0 +1,54 @@
"""Verify exact installed pins/notices, immutable vendor files and browser graph."""
import hashlib
import json
from pathlib import Path
root = Path(__file__).resolve().parent
lock = json.loads((root / 'package-lock.json').read_text())
manifest = json.loads((root / 'package.json').read_text())
inventory = (root / 'LICENSE_INVENTORY.md').read_text()
notices = (root / 'THIRD_PARTY_NOTICES.md').read_text()
installed = 0
for section in ['dependencies', 'devDependencies']:
for name, version in manifest[section].items():
assert lock['packages']['node_modules/' + name]['version'] == version
for path, entry in lock['packages'].items():
if not path:
continue
name = path.split('node_modules/')[-1]
assert entry['resolved'].startswith('https://registry.npmjs.org/')
assert entry['integrity'].startswith('sha512-')
assert f"| {name} | {entry['version']} | {entry['license']} |" in inventory
package = root / path
if not package.exists():
assert entry.get('optional'), name
continue
meta = json.loads((package / 'package.json').read_text())
assert meta['version'] == entry['version'], name
assert meta['license'] == entry['license'], name
licenses = [f for f in package.iterdir() if f.is_file() and
('license' in f.name.lower() or f.name.lower().startswith(('copying', 'notice')))]
if name == '@rolldown/binding-darwin-arm64':
assert not licenses
licenses = [root / 'node_modules/rolldown/LICENSE',
root / 'node_modules/rolldown/THIRD-PARTY-LICENSE']
assert licenses, name
for file in licenses:
assert file.read_text().rstrip() in notices, file
installed += 1
# Parent is read-only evidence of unmodified pinned candidate, not a dependency.
original = root.parent / 'node_modules/stanza'
current = root / 'node_modules/stanza'
files = [f for f in current.rglob('*') if f.is_file()]
assert {f.relative_to(current) for f in files} == {
f.relative_to(original) for f in original.rglob('*') if f.is_file()}
for file in files:
assert file.read_bytes() == (original / file.relative_to(current)).read_bytes(), file
modules = json.loads((root / 'dist/modules.json').read_text())
assert any(x.endswith('/stanza/platform/browser/crypto/Hmac.js') for x in modules)
assert not any('/platform/node/' in x or x.startswith('node:') or 'browser-external' in x for x in modules)
print(f"{len(lock['packages']) - 1} locked entries; {installed} installed licenses/pins; "
f"{len(files)} unchanged Stanza files; browser backend confirmed")
for name in ['package-lock.json', 'sha512-mechanism.mjs', 'validated-sasl.mjs',
'test/scramp-vectors.json', 'dist/browser.mjs', 'dist/modules.json']:
print(name, hashlib.sha256((root / name).read_bytes()).hexdigest())
@@ -0,0 +1,2 @@
scramp==1.4.6 --hash=sha256:a0cf9d2b4624b69bac5432dd69fecfc55a542384fe73c3a23ed9b138cda484e1
asn1crypto==1.5.1 --hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67
@@ -0,0 +1,39 @@
"""Independent existing SCRAM client/server; synthetic public fixture data only."""
import base64
import json
import sys
from importlib.metadata import version
from pathlib import Path
from scramp import ScramClient, ScramMechanism
assert version("scramp") == "1.4.6"
assert version("asn1crypto") == "1.5.1"
path = Path(__file__).parent / "test" / "scramp-vectors.json"
nonce = "rOprNGfwEbeRWgbNEkqO"
server_nonce = "%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0"
salt = base64.b64decode("W22ZaJ0SNY7soEsUEjb6gQ==")
results = []
for password in ["pencil", "A" * 131, "p\u00e9ncil"]:
mechanism = ScramMechanism("SCRAM-SHA-512")
auth = mechanism.make_auth_info(password, iteration_count=4096, salt=salt)
def auth_fn(username):
assert username == "user"
return auth
server = mechanism.make_server(auth_fn, s_nonce=server_nonce)
client = ScramClient(["SCRAM-SHA-512"], "user", password, c_nonce=nonce)
initial = client.get_client_first()
server.set_client_first(initial)
first = server.get_server_first()
client.set_server_first(first)
proof = client.get_client_final()
server.set_client_final(proof) # Independent server verifies proof.
final = server.get_server_final()
client.set_server_final(final) # Independent client verifies server.
results.append(dict(mechanism="SCRAM-SHA-512", username="user", password=password,
nonce=nonce, initial=initial, first=first, proof=proof, final=final))
if sys.argv[1:] == ["--write"]:
path.write_text(json.dumps(results, indent=2) + "\n")
else:
assert not sys.argv[1:]
assert json.loads(path.read_text()) == results
print("scramp 1.4.6: three fixed SHA-512 client/server exchanges verified")
@@ -0,0 +1,925 @@
{
"name": "luma-browser-scram-sha512-experiment",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "luma-browser-scram-sha512-experiment",
"version": "0.0.0",
"dependencies": {
"events": "3.3.0",
"stanza": "12.22.1"
},
"devDependencies": {
"vite": "8.3.4"
},
"engines": {
"node": "24.21.0",
"npm": "11.19.0"
}
},
"node_modules/@oxc-project/types": {
"version": "0.154.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz",
"integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/oxc-project"
}
},
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz",
"integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz",
"integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz",
"integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz",
"integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz",
"integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz",
"integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz",
"integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz",
"integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz",
"integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz",
"integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz",
"integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz",
"integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz",
"integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz",
"integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz",
"integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.20.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz",
"integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/punycode": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/punycode/-/punycode-2.1.4.tgz",
"integrity": "sha512-trzh6NzBnq8yw5e35f8xe8VTYjqM3NE7bohBtvDVf/dtUer3zYTLK1Ka3DG3p7bdtoaOHZucma6FfVKlQ134pQ==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/lightningcss": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.33.0",
"lightningcss-darwin-arm64": "1.33.0",
"lightningcss-darwin-x64": "1.33.0",
"lightningcss-freebsd-x64": "1.33.0",
"lightningcss-linux-arm-gnueabihf": "1.33.0",
"lightningcss-linux-arm64-gnu": "1.33.0",
"lightningcss-linux-arm64-musl": "1.33.0",
"lightningcss-linux-x64-gnu": "1.33.0",
"lightningcss-linux-x64-musl": "1.33.0",
"lightningcss-win32-arm64-msvc": "1.33.0",
"lightningcss-win32-x64-msvc": "1.33.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/nanoid": {
"version": "3.3.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
"integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz",
"integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.20",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/rolldown": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz",
"integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.154.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm-eabi": "1.2.14",
"@rolldown/binding-android-arm64": "1.2.14",
"@rolldown/binding-darwin-arm64": "1.2.14",
"@rolldown/binding-darwin-x64": "1.2.14",
"@rolldown/binding-freebsd-x64": "1.2.14",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.14",
"@rolldown/binding-linux-arm64-gnu": "1.2.14",
"@rolldown/binding-linux-arm64-musl": "1.2.14",
"@rolldown/binding-linux-ppc64-gnu": "1.2.14",
"@rolldown/binding-linux-s390x-gnu": "1.2.14",
"@rolldown/binding-linux-x64-gnu": "1.2.14",
"@rolldown/binding-linux-x64-musl": "1.2.14",
"@rolldown/binding-openharmony-arm64": "1.2.14",
"@rolldown/binding-win32-arm64-msvc": "1.2.14",
"@rolldown/binding-win32-x64-msvc": "1.2.14"
}
},
"node_modules/sdp": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.2.tgz",
"integrity": "sha512-xZocWwfyp4hkbN4hLWxMjmv2Q8aNa9MhmOZ7L9aCZPT+dZsgRr6wZRrSYE3HTdyk/2pZKPSgqI7ns7Een1xMSA==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/stanza": {
"version": "12.22.1",
"resolved": "https://registry.npmjs.org/stanza/-/stanza-12.22.1.tgz",
"integrity": "sha512-WwoSl3u5cCEAbed5ao+yt8ByH6AsBJcPDY0gax3JGQkBX/7wEZ2oxu9nMJ7O1tm8slDyH4FBYIpeluHrT+g/LA==",
"license": "MIT",
"dependencies": {
"@types/node": "^22.0.0",
"@types/punycode": "^2.1.0",
"@types/ws": "^8.18.1",
"punycode": "^2.1.1",
"sdp": "^3.0.2",
"ws": "^8.18.2"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/vite": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz",
"integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.7",
"postcss": "^8.5.29",
"rolldown": "~1.2.12",
"tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.7.1",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"@vitejs/devtools": {
"optional": true
},
"esbuild": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/ws": {
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
"integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}
@@ -0,0 +1,22 @@
{
"name": "luma-browser-scram-sha512-experiment",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "24.21.0",
"npm": "11.19.0"
},
"packageManager": "npm@11.19.0",
"scripts": {
"build": "vite build",
"test": "npm run build && node --test test/*.test.mjs"
},
"dependencies": {
"stanza": "12.22.1",
"events": "3.3.0"
},
"devDependencies": {
"vite": "8.3.4"
}
}
@@ -0,0 +1,28 @@
import sasl from 'stanza/lib/sasl/index.js';
// Public constructor selects the existing browser backend's supported hash alias.
// The delegate name never goes on the wire. No private field is read or changed.
export class Sha512Mechanism {
name = 'SCRAM-SHA-512';
providesMutualAuthentication = true;
#delegate;
constructor(name) {
if (name !== this.name) throw new Error('Unsupported mechanism');
this.#delegate = new sasl.SCRAM('SCRAM-SHA512');
}
getExpectedCredentials() { return this.#delegate.getExpectedCredentials(); }
// Keep derived credentials attempt-local; no cache leaves this boundary.
getCacheableCredentials() { return null; }
createResponse(credentials) {
if (credentials.tlsUnique) throw new Error('Channel binding unavailable');
return this.#delegate.createResponse(credentials);
}
processChallenge(value) { this.#delegate.processChallenge(value); }
processSuccess(value) { this.#delegate.processSuccess(value); }
finalize() { return this.#delegate.finalize(); }
}
export function registerSha512(client) {
client.sasl.disable('SCRAM-SHA-512');
client.sasl.register('SCRAM-SHA-512', Sha512Mechanism, 1000);
}
@@ -0,0 +1,83 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {readFileSync} from 'node:fs';
import {createHash as oracleHash, createHmac as oracleHmac} from 'node:crypto';
import {Buffer as BrowserBuffer, createHash, createHmac, backend, StockScram, Sha512Mechanism, randomBytes} from '../dist/browser.mjs';
const hmacVectors = JSON.parse(readFileSync(new URL('./hmac-vectors.json', import.meta.url)));
const scramVectors = JSON.parse(readFileSync(new URL('./scramp-vectors.json', import.meta.url)));
test('bundle resolves browser backend and contains no external runtime imports', () => {
assert.equal(backend, 'browser');
const modules = JSON.parse(readFileSync(new URL('../dist/modules.json', import.meta.url)));
assert.ok(modules.some(id => id.endsWith('/platform/browser/crypto/Hmac.js')));
assert.ok(modules.some(id => id.endsWith('/platform/browser/crypto/SHA-512.js')));
assert.ok(!modules.some(id => /platform\/node\/|^node:|browser-external/.test(id)));
const bundle = readFileSync(new URL('../dist/browser.mjs', import.meta.url), 'utf8');
assert.ok(!/^import\s|\brequire\(/m.test(bundle));
});
for (const vector of hmacVectors) {
for (const algorithm of ['sha512', 'SHA512']) {
test(`RFC 4231 case ${vector.case}, browser ${algorithm}`, () => {
const key = BrowserBuffer.from(vector.key, 'hex');
const data = BrowserBuffer.from(vector.data, 'hex');
const actual = createHmac(algorithm, key).update(data).digest('hex');
assert.equal(actual.slice(0, vector.digest.length), vector.digest);
});
}
}
test('reproduce browser hyphenated HMAC mismatch, keep Node independent control', () => {
const vector = hmacVectors[0];
const key = BrowserBuffer.from(vector.key, 'hex');
const data = BrowserBuffer.from(vector.data, 'hex');
const broken = createHmac('sha-512', key).update(data).digest('hex');
assert.notEqual(broken, vector.digest);
assert.equal(broken, '9656975ee5de55e75f2976ecce9a04501060b9dc22a6eda2eaef638966280182477fe09f080b2bf564649cad42af8607a2bd8d02979df3a980f15e2326a0a22a');
assert.equal(oracleHmac('sha512', Buffer.from(vector.key, 'hex')).update(Buffer.from(vector.data, 'hex')).digest('hex'), vector.digest);
});
const abc = 'ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192' +
'992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f';
for (const algorithm of ['sha512', 'sha-512', 'SHA512', 'SHA-512']) {
test(`fixed SHA512 abc digest, browser ${algorithm}`, () => {
assert.equal(createHash(algorithm).update('abc').digest('hex'), abc);
});
}
for (const length of [0, 1, 63, 64, 65, 127, 128, 129, 131, 256]) {
test(`browser HMAC alias key length ${length} agrees with Node crypto`, () => {
const key = Buffer.alloc(length, 0xab);
const data = Buffer.alloc(257, 0xcd);
assert.equal(createHmac('sha512', BrowserBuffer.from(key)).update(BrowserBuffer.from(data)).digest('hex'),
oracleHmac('sha512', key).update(data).digest('hex'));
assert.equal(createHash('sha512').update(BrowserBuffer.from(data)).digest('hex'),
oracleHash('sha512').update(data).digest('hex'));
});
}
test('stock browser SHA512 proof mismatch; public alias delegate matches independent vector', () => {
const vector = scramVectors[0];
const credentials = {username: vector.username, password: vector.password, clientNonce: vector.nonce};
const stock = new StockScram('SCRAM-SHA-512');
stock.createResponse(credentials);
stock.processChallenge(BrowserBuffer.from(vector.first));
assert.notEqual(stock.createResponse(credentials).toString(), vector.proof);
const selected = new Sha512Mechanism('SCRAM-SHA-512');
assert.equal(selected.name, vector.mechanism);
assert.equal(selected.createResponse(credentials).toString(), vector.initial);
selected.processChallenge(BrowserBuffer.from(vector.first));
assert.equal(selected.createResponse(credentials).toString(), vector.proof);
selected.processSuccess(BrowserBuffer.from(vector.final));
assert.deepEqual(selected.finalize(), {authenticated: true, mutuallyAuthenticated: true});
assert.equal(selected.getCacheableCredentials(), null);
});
test('native browser RNG path works without fixed nonce, no invented channel binding', () => {
assert.equal(randomBytes(32).length, 32);
const selected = new Sha512Mechanism('SCRAM-SHA-512');
const initial = selected.createResponse({username: 'user', password: 'pencil'}).toString();
assert.match(initial, /^n,,n=user,r=[0-9a-f]{64}$/);
assert.throws(() => new Sha512Mechanism('SCRAM-SHA-512-PLUS'));
assert.throws(() => new Sha512Mechanism('SCRAM-SHA-512').createResponse({tlsUnique: BrowserBuffer.alloc(32)}));
});
@@ -0,0 +1,44 @@
[
{
"case": 1,
"key": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"data": "4869205468657265",
"digest": "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854"
},
{
"case": 2,
"key": "4a656665",
"data": "7768617420646f2079612077616e7420666f72206e6f7468696e673f",
"digest": "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737"
},
{
"case": 3,
"key": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"data": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"digest": "fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb"
},
{
"case": 4,
"key": "0102030405060708090a0b0c0d0e0f10111213141516171819",
"data": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
"digest": "b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd"
},
{
"case": 5,
"key": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"data": "546573742057697468205472756e636174696f6e",
"digest": "415fad6271580a531d4179bc891d87a6"
},
{
"case": 6,
"key": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"data": "54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374",
"digest": "80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598"
},
{
"case": 7,
"key": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"data": "5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e",
"digest": "e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58"
}
]
@@ -0,0 +1,302 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {readFileSync} from 'node:fs';
import {Client, parse, validatedSasl, StockScram} from '../dist/browser.mjs';
const vectors = JSON.parse(readFileSync(new URL('./scramp-vectors.json', import.meta.url)));
const tick = () => new Promise(resolve => setImmediate(resolve));
const deferred = () => {
let resolve;
const promise = new Promise(r => {resolve = r;});
return {promise, resolve};
};
function harness(vector = vectors[1], fixed = true, mechanisms = [vector.mechanism]) {
const client = new Client({jid: 'user@example.invalid', autoReconnect: false,
credentials: {username: vector.username, password: vector.password, clientNonce: vector.nonce}});
const result = {sent: [], success: 0, failed: 0, restart: 0, disconnect: 0, verification: []};
// Capture fixture effects; use the real Client, XML registry, feature dispatcher,
// mechanism factory and SCRAM crypto. Never connect to a real provider.
client.send = async (kind, stanza) => {result.sent.push({kind, stanza});};
client.transport = {authenticated: false, restart: () => {result.restart++;}};
client.disconnect = async () => {result.disconnect++;};
client.on('auth:success', () => {result.success++;});
client.on('auth:failed', () => {result.failed++;});
if (!fixed) client.sasl.register('SCRAM-SHA-512', StockScram, 1000);
if (fixed) {
client.use(validatedSasl);
const original = client.sasl.createMechanism.bind(client.sasl);
client.sasl.createMechanism = names => {
const mech = original(names);
if (mech) for (const method of ['processSuccess', 'finalize']) {
const fn = mech[method].bind(mech);
mech[method] = (...args) => {result.verification.push(method); return fn(...args);};
}
return mech;
};
}
const receive = async (type, value) => {
const body = value === undefined ? '' : Buffer.from(value).toString('base64');
const xml = parse(`<${type} xmlns="urn:ietf:params:xml:ns:xmpp-sasl">${body}</${type}>`);
const stanza = client.stanzas.import(xml);
assert.equal(stanza.type, type);
client.emit('sasl', stanza);
await tick();
};
const start = async () => {
client.emit('features', {sasl: {mechanisms}});
await tick();
};
return {client, result, receive, start};
}
function rejected(h) {
assert.equal(h.result.success, 0);
assert.equal(h.result.restart, 0);
assert.equal(h.result.failed, 1);
assert.equal(h.result.disconnect, 1);
assert.equal(h.client.features.negotiated.sasl, false);
assert.equal(h.client.transport.authenticated, false);
assert.equal(h.client.listenerCount('sasl'), 0);
}
for (const vector of vectors) {
test(`${vector.mechanism}: independent Scramp proof and verified final through feature dispatcher`, async () => {
const h = harness(vector);
await h.start();
assert.equal(h.result.sent[0].stanza.mechanism, vector.mechanism);
assert.equal(h.result.sent[0].stanza.value.toString(), `n,,n=user,r=${vector.nonce}`);
await h.receive('challenge', vector.first);
assert.equal(h.result.sent[1].stanza.value.toString(), vector.proof);
assert.equal(h.result.success, 0);
assert.equal(h.result.restart, 0);
assert.equal(h.client.transport.authenticated, false);
await h.receive('success', vector.final);
assert.deepEqual(h.result.verification, ['processSuccess', 'finalize']);
assert.equal(h.result.success, 1);
assert.equal(h.result.restart, 1);
assert.equal(h.result.disconnect, 0);
assert.equal(h.client.features.negotiated.sasl, true);
assert.equal(h.client.transport.authenticated, true);
await h.receive('success', vector.final);
assert.equal(h.result.success, 1);
assert.equal(h.result.restart, 1);
});
for (const [label, final] of [
['missing', undefined],
['forged', 'v=' + (vector.final[2] === 'A' ? 'B' : 'A') + vector.final.slice(3)],
['error', 'e=invalid-proof'],
['conflicting error', `${vector.final},e=invalid-proof`],
['duplicate verifier', `${vector.final},${vector.final}`],
['bad base64', 'v=***'],
['oversized', 'v=' + 'A'.repeat(4096)]
]) {
test(`${vector.mechanism}: reject ${label} server-final after proof`, async () => {
const h = harness(vector);
await h.start();
await h.receive('challenge', vector.first);
await h.receive('success', final);
rejected(h);
if (label === 'forged') assert.deepEqual(h.result.verification, ['processSuccess', 'finalize']);
await h.receive('success', vector.final);
rejected(h);
});
}
test(`${vector.mechanism}: reject valid-looking final before proof`, async () => {
const h = harness(vector);
await h.start();
await h.receive('success', vector.final);
rejected(h);
assert.deepEqual(h.result.verification, []);
});
}
for (const [label, final] of [['missing', undefined], ['forged', 'v=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=']]) {
test(`stock 12.22.1 reproduces ${label} premature acceptance`, async () => {
const h = harness(vectors[1], false);
await h.start();
await h.receive('success', final);
assert.equal(h.result.success, 1);
assert.equal(h.result.restart, 1);
assert.equal(h.client.transport.authenticated, true);
});
test(`replacement rejects original ${label} premature defect`, async () => {
const h = harness();
await h.start();
await h.receive('success', final);
rejected(h);
});
}
for (const [label, first] of [
['wrong nonce', vectors[1].first.replace('r=rOpr', 'r=wrong')],
['unextended nonce', `r=${vectors[1].nonce},s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096`],
['iteration overflow', vectors[1].first.replace('i=4096', 'i=100001')],
['invalid iterations', vectors[1].first.replace('i=4096', 'i=NaN')],
['duplicate attribute', vectors[1].first + ',i=4096'],
['mandatory extension', 'm=required,' + vectors[1].first]
]) {
test(`reject ${label} before library proof work`, async () => {
const h = harness();
await h.start();
await h.receive('challenge', first);
rejected(h);
assert.equal(h.result.sent.length, 1);
});
}
test('reject success while proof send is pending; no late revival', async () => {
const h = harness();
await h.start();
const pending = deferred();
h.client.send = () => pending.promise;
await h.receive('challenge', vectors[1].first);
await h.receive('success', vectors[1].final);
rejected(h);
pending.resolve();
await tick();
await h.receive('success', vectors[1].final);
rejected(h);
});
test('duplicate challenge while sending proof fails closed', async () => {
const h = harness();
await h.start();
const pending = deferred();
h.client.send = () => pending.promise;
await h.receive('challenge', vectors[1].first);
await h.receive('challenge', vectors[1].first);
rejected(h);
pending.resolve();
await tick();
rejected(h);
});
test('reset while awaiting credentials prevents late auth and listener leak', async () => {
const h = harness();
const pending = deferred();
h.client.getCredentials = () => pending.promise;
await h.start();
h.client.emit('--reset-stream-features');
pending.resolve({username: 'user', password: 'pencil', clientNonce: vectors[1].nonce});
await tick();
await h.receive('success', vectors[1].final);
assert.equal(h.result.sent.length, 0);
assert.equal(h.result.success, 0);
assert.equal(h.result.restart, 0);
assert.equal(h.client.listenerCount('sasl'), 0);
});
test('reset after proof, new negotiation succeeds without old handlers', async () => {
const h = harness();
await h.start();
await h.receive('challenge', vectors[1].first);
h.client.emit('--reset-stream-features');
await h.receive('success', vectors[1].final);
assert.equal(h.result.success, 0);
assert.equal(h.client.listenerCount('sasl'), 0);
await h.start();
assert.equal(h.client.listenerCount('sasl'), 1);
await h.receive('challenge', vectors[1].first);
await h.receive('success', vectors[1].final);
assert.equal(h.result.success, 1);
assert.equal(h.result.restart, 1);
});
test('fixture policy filters PLUS and plaintext; strongest available non-PLUS selected', async () => {
const h = harness(vectors[1], true, ['PLAIN', 'SCRAM-SHA-1', 'SCRAM-SHA-512-PLUS', 'SCRAM-SHA-256', 'SCRAM-SHA-512']);
await h.start();
assert.equal(h.result.sent[0].stanza.mechanism, 'SCRAM-SHA-512');
});
for (const names of [['PLAIN'], ['SCRAM-SHA-512-PLUS'], []]) {
test(`no allowed mechanism (${names.join(',')}) disconnects`, async () => {
const h = harness(vectors[1], true, names);
await h.start();
rejected(h);
assert.equal(h.result.sent.length, 0);
});
}
for (const type of ['failure', 'abort']) {
test(`${type} is terminal; valid final cannot revive it`, async () => {
const h = harness();
await h.start();
await h.receive(type);
await h.receive('success', vectors[1].final);
rejected(h);
});
}
test('success during pending initial credentials fails closed', async () => {
const h = harness();
const pending = deferred();
h.client.getCredentials = () => pending.promise;
await h.start();
await h.receive('success', vectors[1].final);
rejected(h);
pending.resolve({username: 'user', password: 'pencil', clientNonce: vectors[1].nonce});
await tick();
assert.equal(h.result.sent.length, 0);
rejected(h);
});
test('reset during proof send cancels continuation', async () => {
const h = harness();
await h.start();
const pending = deferred();
h.client.send = () => pending.promise;
await h.receive('challenge', vectors[1].first);
h.client.emit('--reset-stream-features');
pending.resolve();
await tick();
await h.receive('success', vectors[1].final);
assert.equal(h.result.success, 0);
assert.equal(h.result.restart, 0);
assert.equal(h.client.listenerCount('sasl'), 0);
});
for (const stage of ['credentials', 'initial send', 'proof send']) {
test(`${stage} rejection disconnects without success`, async () => {
const h = harness();
const reject = async () => {throw new Error('synthetic fixture failure');};
if (stage === 'credentials') h.client.getCredentials = reject;
if (stage === 'initial send') h.client.send = reject;
await h.start();
if (stage === 'proof send') {
h.client.send = reject;
await h.receive('challenge', vectors[1].first);
}
rejected(h);
});
}
test('repeat challenge after completed proof is rejected', async () => {
const h = harness();
await h.start();
await h.receive('challenge', vectors[1].first);
await h.receive('challenge', vectors[1].first);
rejected(h);
});
test('synthetic binding input fails without auth or plaintext fallback', async () => {
const h = harness();
h.client.config.credentials.tlsUnique = Buffer.alloc(32);
await h.start();
rejected(h);
assert.equal(h.result.sent.length, 0);
});
test('server silence after proof never authenticates; reset discards attempt', async () => {
const h = harness();
let cacheEvents = 0;
h.client.on('credentials:update', () => {cacheEvents++;});
await h.start();
await h.receive('challenge', vectors[0].first);
assert.equal(h.result.success, 0);
assert.equal(h.result.restart, 0);
assert.equal(h.client.transport.authenticated, false);
assert.equal(cacheEvents, 0);
assert.equal(h.client.config.credentials.saltedPassword, undefined);
h.client.emit('--reset-stream-features');
await h.receive('success', vectors[0].final);
assert.equal(h.result.success, 0);
assert.equal(h.result.restart, 0);
});
@@ -0,0 +1,32 @@
[
{
"mechanism": "SCRAM-SHA-512",
"username": "user",
"password": "pencil",
"nonce": "rOprNGfwEbeRWgbNEkqO",
"initial": "n,,n=user,r=rOprNGfwEbeRWgbNEkqO",
"first": "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096",
"proof": "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=gMGXRcevScNtxZ6/8lQYpGtnsNAc3mGcmNomv+xnoOMw+3R2xNJdMNnzMlTN8PPC6wdp6dybEmDYXYTxwnYPJQ==",
"final": "v=ZQnYEgWQMFmmsM8aQMF0nDDCy/AgCzkwk8CmMZYcMg0vSVlKDanekLtifDSeVGT4+5ZxXnJq199RVG2rR7N7Zw=="
},
{
"mechanism": "SCRAM-SHA-512",
"username": "user",
"password": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"nonce": "rOprNGfwEbeRWgbNEkqO",
"initial": "n,,n=user,r=rOprNGfwEbeRWgbNEkqO",
"first": "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096",
"proof": "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=FQ+Ds6nMR5kU+jnkwyhJGBK8lA2hTFKTqEBVSUCvSgmexvd4GGT4uAqD3iACaCkLYP0Bc6JHwnHMWkpGNOX2rg==",
"final": "v=dXX1mz64UYbnX15NloHSoWfqoMVVvFg23i8KXwHigIVa+U6w+eJBBwhNg47qeIqkRLhhVnRnvcVvQMXpqav+4A=="
},
{
"mechanism": "SCRAM-SHA-512",
"username": "user",
"password": "p\u00e9ncil",
"nonce": "rOprNGfwEbeRWgbNEkqO",
"initial": "n,,n=user,r=rOprNGfwEbeRWgbNEkqO",
"first": "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096",
"proof": "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=Jwk58tl/01ikMIUvnr0MXiTPWZJOiqtTyIAKx2A7IL85TXsL9S/fvBEIYvd/a0QIAQrNp47BSvResR5QB63TSg==",
"final": "v=QwFhP0JOInTkJ/uTsZ2DRt00V0dQtkZKotpq815cU8X2SpF/5qbOd8sd6TCj8NjX/GPke8Xj4yg6whLHu7KNlw=="
}
]
@@ -0,0 +1,111 @@
// Isolated S00-019 copy of the reviewed S00-017 ordering guard.
import {registerSha512} from './sha512-mechanism.mjs';
// No production transport, credential persistence, or channel binding is provided.
const SUPPORTED = new Set(['SCRAM-SHA-512']);
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
function text(value) {
// SASL's parsed additional-data is a Buffer, not raw XML/base64 text.
if (!value || typeof value.toString !== 'function' || value.length > 4096) {
throw new Error('Invalid SASL data');
}
const result = value.toString('utf8');
if (!/^[\x21-\x7e]*$/.test(result)) throw new Error('Invalid SASL data');
return result;
}
function firstMessage(value, nonce) {
// Deliberately narrow, no extensions. Bound work before calling library crypto.
const match = /^r=([\x21-\x2b\x2d-\x7e]+),s=([A-Za-z0-9+/=]+),i=([1-9][0-9]*)$/.exec(text(value));
if (!match || !BASE64.test(match[2]) || !match[1].startsWith(nonce) ||
match[1].length <= nonce.length || Number(match[3]) > 100000) {
throw new Error('Invalid SCRAM server-first');
}
}
function finalMessage(value, name) {
const final = text(value);
if (name !== 'SCRAM-SHA-512') throw new Error('Unsupported mechanism');
const length = 88;
if (!final.startsWith('v=') || final.length !== length + 2 || !BASE64.test(final.slice(2))) {
throw new Error('Invalid SCRAM server-final');
}
}
export default function validatedSasl(client) {
registerSha512(client);
let active;
// Public plugin + named feature registration replaces the stock handler before use.
client.registerFeature('sasl', 100, async (features, done) => {
if (active) active();
const names = features.sasl.mechanisms.filter(name => SUPPORTED.has(name));
const mech = client.sasl.createMechanism(names);
let phase = 'starting';
let credentials;
let nonce;
const cleanup = () => {
client.off('sasl', handler);
client.off('--reset-stream-features', reset);
if (active === reset) active = undefined;
};
const reset = () => {
phase = 'closed';
cleanup();
client.features.negotiated.sasl = false;
};
const fail = () => {
if (phase === 'closed') return;
phase = 'closed';
cleanup();
client.features.negotiated.sasl = false;
if (client.transport) client.transport.authenticated = false;
client.emit('auth:failed');
done('disconnect', 'SCRAM validation failed'); // Never log peer data/credentials.
};
const handler = async sasl => {
if (phase === 'closed') return;
try {
if (sasl.type === 'challenge' && phase === 'server-first') {
phase = 'sending-proof'; // Reject duplicate challenges and racing success.
firstMessage(sasl.value, nonce);
mech.processChallenge(sasl.value);
const response = mech.createResponse(credentials);
await client.send('sasl', {type: 'response', value: response});
if (phase !== 'closed') phase = 'server-final';
return;
}
if (sasl.type !== 'success' || phase !== 'server-final') return fail();
finalMessage(sasl.value, mech.name);
mech.processSuccess(sasl.value);
const result = mech.finalize();
if (!result.authenticated || !result.mutuallyAuthenticated) return fail();
phase = 'closed';
cleanup();
client.features.negotiated.sasl = true;
if (client.transport) client.transport.authenticated = true;
client.emit('auth:success', client.config.credentials);
done('restart');
} catch {
fail();
}
};
active = reset;
client.features.negotiated.sasl = false;
client.on('sasl', handler);
client.once('--reset-stream-features', reset);
try {
if (!mech || !SUPPORTED.has(mech.name)) return fail();
credentials = await client.getCredentials();
if (phase === 'closed') return;
// Restrict this experiment to non-PLUS; never invent TLS binding bytes.
if (credentials.tlsUnique) return fail();
const response = mech.createResponse(credentials);
nonce = /,r=([^,]+)$/.exec(response.toString('utf8'))?.[1];
if (!nonce) return fail();
await client.send('sasl', {type: 'auth', mechanism: mech.name, value: response});
if (phase !== 'closed') phase = 'server-first';
} catch {
fail();
}
});
}
@@ -0,0 +1,23 @@
import {defineConfig} from 'vite';
import {fileURLToPath} from 'node:url';
export default defineConfig({
resolve: {alias: {events: fileURLToPath(new URL('./node_modules/events/events.js', import.meta.url))}},
build: {
target: ['chrome111', 'edge111', 'firefox114', 'safari16.4'],
minify: false,
lib: {entry: 'browser-entry.mjs', formats: ['es'], fileName: () => 'browser.mjs'},
rollupOptions: {plugins: [{
name: 'backend-evidence',
generateBundle() {
const modules = [...this.getModuleIds()].map(id => id.replace(process.cwd(), '.')).sort();
if (!modules.some(id => id.endsWith('/stanza/platform/browser/crypto/Hmac.js'))) {
throw new Error('Browser HMAC missing');
}
if (modules.some(id => /stanza\/platform\/node\/|^node:|browser-external/.test(id))) {
throw new Error('Unexpected Node runtime backend');
}
this.emitFile({type: 'asset', fileName: 'modules.json', source: JSON.stringify(modules, null, 2) + '\n'});
}
}]}
}
});