S00-003: Evaluate Stanza with verified SCRAM and bounded WSS ingress

Changes: Propose the pinned XMPP library decision and add an isolated browser-built selection spike combining reviewed public SASL, SHA-512 and parser seams. Exercise TLS/xmpp handshake, strongest compatible non-PLUS authentication, rejected unsafe paths and extension mappings; document duplicate-body export projection and remaining limits. Preserve dependency notices, evidence and QA handoff.
Validation: Clean-cache pinned npm ci succeeded; npm test passed 112 checks; check-dependencies.py verified 51 locked entries, 26 installed packages and 340 unchanged vendor files; make check passed 72 tooling tests. No actual browser runtime, independent full XMPP server/client or production encryption/release readiness claim.

Task: S00-003
Agent: auto-protocol-f474114f0168
Role: protocol
Action: claim
Status: review

Task title: Проверить XMPP WSS/auth библиотеку
This commit is contained in:
wt committed 2026-10-15 18:20:05 +07:00
1 parent e08c0ab590
commit 51a414d386
28 files changed
+6709 -32

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
# ADR 0002: XMPP library and required browser integration seams
- Status: proposed; S00-003 independent QA review pending.
- Date: 2026-10-15. Owner: auto-protocol-f474114f0168 (protocol).
- Reviewer role: qa; no acceptance claimed by executor.
- Requirements: ACC-004, NET-003, NET-004.
- Prerequisites: accepted scope/stack ADRs and bounded SASL, parser, SHA-512
experiments (S00-017/018/019); independently reviewed Linux environment S00-020.
## Question and decision
Select Stanza 12.22.1 (MIT), with the three reviewed public integration seams
mandatory before connect: bounded transport, fail-closed SASL feature, and the
SHA-512 mechanism composition. Stock Stanza alone is unsuitable for this profile.
Its stock SASL handler does not verify server-final, its browser hyphenated
SHA-512 HMAC is wrong, and its persistent parser has no finite resource budgets.
The accompanying spike proves the composed candidate; it creates no production
adapter, shared contract, crypto implementation or provider deployment.
| Option | Evidence and tradeoff |
| --- | --- |
| Stanza 12.22.1 plus reviewed extensions | Selected: actual browser bundle, public Client/Factory/Registry/Transport APIs, verified fixed SCRAM transcripts, loopback WSS and extension fixtures. JSON models and built-in MAM/MUC/Jingle/upload mappings fit the planned adapter boundary. Exact-version alias and replacement plugins require regression checks on upgrades. |
| Stock Stanza | Rejected: the accepted remediation experiments reproduce security/framing defects. Installing the package alone does not meet this decision. |
| xmpp.js | Viable alternative with browser WebSocket and modular packages; its primary README documents MIT and client/component modules. Not installed or executed here; no superiority/security finding or exact pin is claimed. It would require a separate equivalent integration experiment, rather than assuming its SASL/parser meet the profile. |
## One runnable selection experiment
`experiments/0002-xmpp-library/selection-spike` combines unchanged reviewed
bounded-ingress and SHA-512 alias source with the ordering guard extended to
non-PLUS SHA-512/256/1. Existing library crypto implements all hashes, HMAC,
SCRAM and SASLprep. No vendor file, private field or primitive is changed.
The real Client.connect, XML registry, feature dispatcher, queue and browser
SCRAM backend run against a synthetic loopback HTTPS/WebSocket peer. Its
self-signed certificate is generated temporarily by OpenSSL, trusted explicitly
by the test socket only, and hostname/certificate checks remain enabled. No
system trust store is changed. An uninjected native WebSocket rejects the same
untrusted certificate; a plaintext endpoint is rejected before a socket exists.
The trusted peer observes TLS plus the xmpp subprotocol, framing open, exact
proof and a second open only after a valid server-final. Forged/premature finals
never authenticate or restart. This is a real WSS handshake with a fixture peer,
not interoperability with a deployed independent XMPP server.
Mechanism policy filters PLUS because browser TLS binding is unavailable,
selects SHA-512 then SHA-256 then SHA-1 independent of offer order, and disables
all other fallbacks. NET-003 permits a configured protected fallback; this
selection enables none. PLUS-only/PLAIN-only/empty offers fail. Diagnostics
contain selected mechanism, `channelBinding: unavailable`, and disabled fallback;
no credential, nonce, proof or peer payload. Synthetic tlsUnique is rejected.
GS2 n,, and c=biws are verified. A future platform with real binding must add a
separately reviewed implementation before offering PLUS.
Parser experiment ceilings are preserved: 4096 UTF-8 bytes, depth 8, 32 elements,
16 attributes, 1024 aggregate attribute units, 128 name units and 256 attribute
value units. Standalone complete text XML is preflighted by saxes before JXT
import. Oversized and split XML fail on the actual WSS path. These numbers are
local experiment limits, not a production message/media-size contract.
Registry fixtures import/export MAM forwarded IDs, MUC join, Jingle terminate,
HTTP upload URLs and legacy opaque OMEMO bytes. An explicitly invented opaque
namespace demonstrates Client.use/Registry.define extension and namespace
isolation. It is not an OMEMO2 implementation or encrypted messaging claim.
Imported messages expose both selected `body` and `alternateLanguageBodies`;
exporting both concatenates text in this version. A regression records it;
the MAM fixture exports only the language collection. Future adapters must
construct deliberate outbound shapes rather than echo imported objects.
## Reproduction, versions and licenses
Follow the experiment README. Exact Node 24.21.0/npm 11.19.0 and Vite 8.3.4 remain
from accepted ADR 0001. Stanza 12.22.1, saxes 6.0.0, events 3.3.0 and test peer
ws 8.22.0 have exact direct pins; lock v3 includes all optional hosts. These are
already reviewed package families, not a new stack. MIT/ISC runtime notices,
Stanza bundled ancestry, and build MIT/ISC/BSD/Apache/MPL notices are preserved
in inventory/notices; saxes' omitted package license is preserved from the
reviewed versioned upstream copy. MPL build code is unmodified. Dependency
monitoring, provenance and advisory clearance remain continuing release work.
Browser syntax floors are the accepted Chrome/Edge 111, Firefox 114, Safari 16.4
build targets, not a product support certification. Bundle graph excludes Node
platform/crypto and unresolved builtins; events is explicitly bundled. Native
WebSocket, secure RNG, URL and TextEncoder are required. Server/test tooling
Node crypto is only an independent oracle; it does not replace browser crypto.
## Primary sources inspected on 2026-10-15
- [RFC 7395](https://www.rfc-editor.org/rfc/rfc7395.html), sections 3.1–3.4,
3.7 and 3.9: subprotocol, complete text XML, framing, restart and TLS.
- [RFC 5802](https://www.rfc-editor.org/rfc/rfc5802.html#section-5) and
[RFC 7677](https://www.rfc-editor.org/rfc/rfc7677.html#section-3): fixed SHA-1/256
exchange vectors and server verification. SHA-512 independent Scramp fixtures
and RFC 4231 primitive vectors retain their accepted S00-019 provenance.
- [Stanza upstream](https://github.com/legastero/stanza) and
[xmpp.js upstream](https://github.com/xmppjs/xmpp.js): candidate APIs, browser
positioning and licenses. Exact installed Stanza 12.22.1 Client.d.ts/.js,
lib/sasl, plugins/features, jxt/Registry and protocol modules are authoritative
for shipped extension behavior; all 340 files match the independently installed
S00-020 vendor graph. Upstream branches do not establish version guarantees.
- [XEP-0313](https://xmpp.org/extensions/xep-0313.html),
[XEP-0045](https://xmpp.org/extensions/xep-0045.html),
[XEP-0166](https://xmpp.org/extensions/xep-0166.html),
[XEP-0363](https://xmpp.org/extensions/xep-0363.html): fixture namespaces and
envelope/extension shapes. Shipped protocol comments identify older revisions;
a passing simple fixture does not establish complete current-XEP coverage.
## Consequences, limits and review
The spike is sufficient input for bounded selection review. Production still
requires S01 contracts and its own implementations/tests. The minimal transport
omits complete stream sequencing, error/close negotiation, redirect and keepalive
policies. Queue/rate/network-allocation bounds, worker/CPU performance, timeout,
full SASL extensions/final-challenge compatibility and constant-time verifier
assurance are not established. Library verifier uses string equality. Strict
success-only final layout and conservative send-race rejection reduce availability.
No Chrome/Firefox/Safari runtime execution, independent XMPP server/client,
real account, OMEMO encryption, production feature semantics, full browser matrix
or release readiness was verified. Browser-built code runs in Node with host web
APIs; socket injection adds fixture CA trust, not a browser channel-binding API.
Old accepted experiment source is unchanged; shared interfaces/specs/stack and
network/TLS/CORS policies remain unchanged. Any API incompatibility on upgrade
requires a new bounded decision, not a private patch or silent downgrade.
QA should reproduce clean install/build/tests and dependency checks, inspect the
stock defect controls, explicit extension projection and scope, then record an
independent decision. Evidence: `state/evidence/S00-003-auto-protocol-f474114f0168.md`.
Coordinator owns transition commits; this ADR remains proposed until review.
@@ -0,0 +1,4 @@
engine-strict=true
strict-ssl=true
ignore-scripts=true
registry=https://registry.npmjs.org/
@@ -0,0 +1,57 @@
# Locked dependency license inventory
Includes optional host entries. No new package families beyond reviewed parser/SHA512 experiments.
| 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 |
| saxes | 6.0.0 | ISC |
| 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 |
| xmlchars | 2.2.0 | MIT |
@@ -0,0 +1,51 @@
# S00-003 combined XMPP selection spike
One isolated experiment; no application adapter or production provider. See
`docs/decisions/0002-xmpp-library.md` for selection rationale and limits.
Use the authentic pinned Node 24.21.0 / npm 11.19.0 from S00-020. On this host its
reviewed temporary bin path was `/tmp/luma-S00-026-j1pmys7w/node-v24.21.0-linux-x64/bin`;
paths may expire. Recreate via the reviewed environment-access probe if needed.
Keep its verified runtime directory first in PATH. OpenSSL must be available.
No system install, trust-store change or secret is needed.
From project root, with the pinned runtime in PATH:
```sh
node --version
npm --version
npm ci --prefix experiments/0002-xmpp-library/selection-spike --ignore-scripts --no-audit --no-fund --engine-strict=true --strict-ssl=true --fetch-retries=0 --fetch-timeout=15000 --cache /tmp/luma-selection-independent-clean
npm test --prefix experiments/0002-xmpp-library/selection-spike
python3 experiments/0002-xmpp-library/selection-spike/check-dependencies.py
make check
```
Use a new empty cache for independent clean-install evidence. The checker compares
vendor bytes against the reviewed `environment-access/node_modules/stanza` graph;
that graph must exist (its verify.py recreates it without altering parent inputs).
No application code or accepted prerequisite source is changed by these commands.
`dist` and node_modules are ignored build artifacts. npm test explicitly invokes
Vite even though install lifecycle scripts are disabled. Lock updates need review.
Tests run the Vite browser artifact in Node. Reused S00-019 regression tests cover
known HMAC vectors, public alias, real feature dispatcher, forged/out-of-order
finals, reset, pending sends, synthetic binding rejection and no derived cache.
WSS tests use a fresh ephemeral localhost certificate/key under /tmp, explicitly
trusted only by the test socket; rejectUnauthorized stays true. A native socket
without that trust must reject it. No certificate/key is committed. The peer
checks fixed public RFC or accepted independent Scramp transcripts; it is a
scripted fixture, not an independent full XMPP implementation. Synthetic fixture
credentials are public/invented data only, never read from the environment.
Bounded ingress and SHA-512 composition are byte-identical reviewed copies.
Only the copied ordering guard extends allowed non-PLUS mechanisms to 512/256/1
and emits safe policy diagnostics. No fallback is enabled. Browser TLS data is
unavailable and never synthesized. Registry tests cover basic MAM/MUC/Jingle/
upload/legacy opaque OMEMO plus an invented extension namespace, not encrypted
interoperability or OMEMO2 wire compliance. They explicitly select the language
collection on export because echoing both library body projections duplicates text;
a separate stock-behavior regression preserves that finding.
No real browser/server/client interoperability, load bound, constant-time audit,
full XEP coverage or product readiness result is implied. All inherited limits
remain in the ADR. Changes are subject to independent QA review.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,146 @@
import {SaxesParser} from 'saxes';
import jxt from 'stanza/jxt/index.js';
// Experiment policy only. The byte ceiling bounds SAX token/namespace storage
// even before callbacks can inspect a completed name or attribute value.
export const LIMITS = Object.freeze({bytes: 4096, depth: 8, elements: 32,
attributes: 16, attributeChars: 1024, nameChars: 128, valueChars: 256});
class IngressError extends Error {
constructor(code) { super(code); this.code = code; }
}
const fail = code => { throw new IngressError(code); };
export function validateMessage(data) {
if (typeof data !== 'string') fail('text-required');
// Avoid encoding an attacker-sized string. UTF-8 bytes >= UTF-16 units for
// valid XML; isolated surrogates are subsequently rejected by saxes.
if (data.length > LIMITS.bytes) fail('bytes');
if (new TextEncoder().encode(data).length > LIMITS.bytes) fail('bytes');
if (!data.startsWith('<')) fail('framing');
let depth = 0, elements = 0, attributes = 0, attributeChars = 0, ended = false;
const parser = new SaxesParser({xmlns: true, fragment: false,
defaultXMLVersion: '1.0', forceXMLVersion: true});
parser.on('error', () => fail('invalid-xml'));
for (const event of ['doctype', 'comment', 'processinginstruction', 'xmldecl']) {
parser.on(event, () => fail('restricted-xml'));
}
parser.on('opentagstart', tag => {
if (++depth > LIMITS.depth) fail('depth');
if (++elements > LIMITS.elements) fail('elements');
if (tag.name.length > LIMITS.nameChars) fail('name');
});
parser.on('attribute', attr => {
if (++attributes > LIMITS.attributes) fail('attributes');
if (attr.name.length > LIMITS.nameChars) fail('name');
if (attr.value.length > LIMITS.valueChars) fail('attribute-value');
attributeChars += attr.name.length + attr.value.length;
if (attributeChars > LIMITS.attributeChars) fail('attribute-chars');
});
parser.on('closetag', () => { depth--; });
parser.on('end', () => { ended = true; });
// close() detects unfinished XML. Throwing from callbacks aborts immediately;
// there is no recovery listener, DTD fetch, custom entity or tree construction.
parser.write(data).close();
if (!ended || depth !== 0 || elements === 0) fail('framing');
}
export function importMessage(data, registry, config) {
validateMessage(data);
// Only bounded, complete documents reach the actual candidate parser/import.
const xml = jxt.parse(data, {allowComments: false});
if (!xml) fail('invalid-xml');
const kind = registry.getImportKey(xml);
const stanza = registry.import(xml, {lang: config.lang,
acceptLanguages: config.acceptLanguages});
if (!kind || !stanza) fail('unknown-root');
return {kind, stanza};
}
// Public Transport interface and Client.transports constructor registry.
// Socket injection is solely for finite fixtures; default is browser WebSocket.
export function installBoundedTransport(client, Socket = globalThis.WebSocket) {
class BoundedTransport {
#client; #registry; #sm; #socket; #config; #closed = true;
hasStream = false;
stream;
constructor(agent, sm, registry) {
this.#client = agent; this.#sm = sm; this.#registry = registry;
}
connect(config) {
if (this.#socket || !this.#closed) throw new Error('Fresh transport required');
if (new URL(config.url).protocol !== 'wss:') throw new Error('WSS required');
this.#config = config;
this.#closed = false;
const socket = this.#socket = new Socket(config.url, 'xmpp');
socket.onopen = () => {
if (this.#closed) return;
if (socket.protocol !== 'xmpp') return this.#reject('subprotocol');
this.#sm.started = false;
this.#client.emit('connected');
this.restart();
};
socket.onmessage = event => {
if (this.#closed) return;
let parsed;
try { parsed = importMessage(event.data, this.#registry, this.#config); }
catch (error) { return this.#reject(error instanceof IngressError ? error.code : 'invalid-xml'); }
// No raw payload event/log or async parser continuation escapes ingress.
if (parsed.kind === 'stream') {
if (parsed.stanza.action === 'open') {
this.hasStream = true;
this.stream = parsed.stanza;
this.#client.emit('stream:start', parsed.stanza);
} else {
this.#client.emit('stream:end');
this.disconnect(false);
}
} else {
this.#client.emit('stream:data', parsed.stanza, parsed.kind);
}
};
socket.onerror = () => this.#reject('socket-error');
socket.onclose = () => this.disconnect(false);
}
#reject(code) {
if (this.#closed) return;
// Close/clear before notifying consumers; a reentrant callback cannot
// revive the attempt. Never expose parser error text containing input.
this.disconnect(false);
this.#client.emit('ingress:rejected', code);
}
disconnect(clean = true) {
if (this.#closed) return;
this.#closed = true;
const socket = this.#socket;
this.#socket = undefined;
if (socket) {
socket.onopen = socket.onmessage = socket.onerror = socket.onclose = null;
if (clean && this.hasStream && socket.readyState === 1) {
socket.send(this.#registry.export('stream', {action: 'close'}).toString());
}
socket.close();
}
this.hasStream = false;
this.stream = undefined;
this.#config = undefined;
if (this.#client.transport === this) this.#client.emit('--transport-disconnected');
}
restart() {
if (this.#closed) return;
this.hasStream = false;
this.stream = undefined;
this.#write(this.#registry.export('stream', {action: 'open',
lang: this.#config.lang, to: this.#config.server, version: '1.0'}).toString());
}
#write(data) {
if (this.#closed || this.#socket?.readyState !== 1) throw new Error('Socket closed');
this.#socket.send(data);
}
async send(name, data) {
const xml = this.#registry.export(name, data);
if (!xml) throw new Error('Unknown outgoing stanza');
this.#write(xml.toString());
}
}
client.transports['bounded-websocket'] = BoundedTransport;
}
@@ -0,0 +1,12 @@
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;
export {installBoundedTransport, validateMessage, importMessage, LIMITS} from './bounded-ingress.mjs';
export const {attribute, text} = jxt;
@@ -0,0 +1,55 @@
"""Check installed locked graph, notices, browser backend and reviewed source reuse."""
import hashlib
import json
from pathlib import Path
root = Path(__file__).resolve().parent
lock = json.loads((root / 'package-lock.json').read_text())['packages']
manifest = json.loads((root / 'package.json').read_text())
inventory = (root / 'LICENSE_INVENTORY.md').read_text()
notices = (root / 'THIRD_PARTY_NOTICES.md').read_text()
for section in ('dependencies', 'devDependencies'):
for name, version in manifest[section].items():
assert lock['node_modules/' + name]['version'] == version
installed = 0
for path, entry in lock.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']
assert meta['license'] == entry['license']
licenses = [f for f in package.iterdir() if f.is_file() and
('license' in f.name.lower() or f.name.lower().startswith(('notice', 'copying')))]
if name == 'saxes':
licenses = [root / 'licenses/saxes-6.0.0-LICENSE']
if not licenses and name.startswith('@rolldown/binding-'):
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
# Independently installed, reviewed locked vendor graph, read only.
original = root.parent / 'environment-access/node_modules/stanza'
current = root / 'node_modules/stanza'
files = {f.relative_to(current) for f in current.rglob('*') if f.is_file()}
assert files == {f.relative_to(original) for f in original.rglob('*') if f.is_file()}
for file in files:
assert (current / file).read_bytes() == (original / file).read_bytes(), file
for local, reviewed in [('bounded-ingress.mjs', 'parser-validation/bounded-ingress.mjs'),
('sha512-mechanism.mjs', 'sha512-validation/sha512-mechanism.mjs'),
('licenses/saxes-6.0.0-LICENSE', 'parser-validation/licenses/saxes-6.0.0-LICENSE')]:
assert (root / local).read_bytes() == (root.parent / reviewed).read_bytes(), local
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)-1} locked / {installed} installed packages; notices verified; {len(files)} unchanged Stanza files; reviewed parser/alias reused; browser backend')
for file in ['package-lock.json', 'validated-sasl.mjs', 'dist/browser.mjs']:
print(file, hashlib.sha256((root / file).read_bytes()).hexdigest())
@@ -0,0 +1,64 @@
The ISC License
Copyright (c) Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
====
The following license is the one that governed sax, from which saxes
was forked. Isaac Schlueter is not *directly* involved with saxes so
don't go bugging him for saxes issues.
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
====
`String.fromCodePoint` by Mathias Bynens is no longer used, but it can
still be found in old commits. It was once used according to terms of
MIT License, as follows:
Copyright Mathias Bynens <https://mathiasbynens.be/>
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,945 @@
{
"name": "luma-xmpp-selection-spike",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "luma-xmpp-selection-spike",
"version": "0.0.0",
"dependencies": {
"events": "3.3.0",
"saxes": "6.0.0",
"stanza": "12.22.1",
"ws": "8.22.0"
},
"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/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"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
}
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT"
}
}
}
@@ -0,0 +1,24 @@
{
"name": "luma-xmpp-selection-spike",
"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",
"saxes": "6.0.0",
"ws": "8.22.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,42 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {Client, importMessage, attribute, text} from '../dist/browser.mjs';
const client=new Client();
const roundtrip=(kind,xml)=>{
const imported=importMessage(xml,client.stanzas,{});
assert.equal(imported.kind,kind);
// The API exposes both a selected body and the full language collection.
// For this fixture choose the language collection as the sole export input;
// passing both representations concatenates text in Stanza 12.22.1.
const output = {...imported.stanza};
if (output.archive?.item?.message) {
output.archive = {...output.archive, item: {...output.archive.item, message: {...output.archive.item.message}}};
delete output.archive.item.message.body;
}
const emitted=client.stanzas.export(kind,output).toString();
const again=importMessage(emitted,client.stanzas,{}).stanza;
return {stanza:imported.stanza,again,emitted};
};
for(const [name,kind,xml,check] of [
['MAM result/forwarded stable IDs','message','<message xmlns="jabber:client"><result xmlns="urn:xmpp:mam:2" id="archive-1" queryid="query-1"><forwarded xmlns="urn:xmpp:forward:0"><message xmlns="jabber:client" id="message-1"><body>synthetic</body><origin-id xmlns="urn:xmpp:sid:0" id="origin-1"/></message></forwarded></result></message>',s=>{assert.equal(s.archive.id,'archive-1');assert.equal(s.archive.item.message.originId,'origin-1');assert.equal(s.archive.item.message.body,'synthetic');}],
['MUC join','presence','<presence xmlns="jabber:client" to="room@conference.example.invalid/user"><x xmlns="http://jabber.org/protocol/muc"/></presence>',s=>assert.equal(s.muc.type,'join')],
['Jingle signaling','iq','<iq xmlns="jabber:client" type="set" id="call-1"><jingle xmlns="urn:xmpp:jingle:1" action="session-terminate" sid="session-1"><reason><success/></reason></jingle></iq>',s=>{assert.equal(s.jingle.action,'session-terminate');assert.equal(s.jingle.sid,'session-1');}],
['HTTP upload slot','iq','<iq xmlns="jabber:client" type="result" id="upload-1"><slot xmlns="urn:xmpp:http:upload:0"><put url="https://upload.example.invalid/put"/><get url="https://upload.example.invalid/get"/></slot></iq>',s=>{assert.equal(s.httpUpload.download,'https://upload.example.invalid/get');}],
['legacy opaque OMEMO encoding','message','<message xmlns="jabber:client"><encrypted xmlns="eu.siacs.conversations.axolotl"><header sid="1"><key rid="2">AA==</key><iv>AA==</iv></header><payload>AA==</payload></encrypted></message>',s=>{assert.equal(s.omemo.header.sid,1);assert.equal(s.omemo.header.keys[0].rid,2);assert.equal(s.omemo.payload.toString('hex'),'00');}]
]) test(`${name}: real registry import/export`,()=>{const r=roundtrip(kind,xml);check(r.stanza);check(r.again);});
test('public plugin registry supports future opaque extension without vendor patch',()=>{
// Deliberately NOT a claimed OMEMO2 wire implementation or encryption result.
client.use(c=>c.stanzas.define({namespace:'urn:luma:selection-spike:opaque',element:'opaque',
path:'message.spikeOpaque',fields:{id:attribute('id'),value:text()}}));
const r=roundtrip('message','<message xmlns="jabber:client"><opaque xmlns="urn:luma:selection-spike:opaque" id="fixture-1">synthetic-data</opaque></message>');
assert.deepEqual(r.stanza.spikeOpaque,{id:'fixture-1',value:'synthetic-data'});
assert.deepEqual(r.again.spikeOpaque,r.stanza.spikeOpaque);
const wrong=importMessage('<message xmlns="jabber:client"><opaque xmlns="urn:luma:wrong" id="fixture-1">ignored</opaque></message>',client.stanzas,{});
assert.equal(wrong.stanza.spikeOpaque,undefined);
});
test('record raw dual-body export behavior requiring explicit projection',()=>{
const r=importMessage('<message xmlns="jabber:client"><body>synthetic</body></message>',client.stanzas,{});
const raw=client.stanzas.export('message',r.stanza).toString();
assert.equal(importMessage(raw,client.stanzas,{}).stanza.body,'syntheticsynthetic');
});
@@ -0,0 +1,19 @@
// Public RFC examples only. Not accounts, secrets, or network endpoints.
export const vectors = [
{
source: 'https://www.rfc-editor.org/rfc/rfc5802.html#section-5',
mechanism: 'SCRAM-SHA-1', username: 'user', password: 'pencil',
nonce: 'fyko+d2lbbFgONRv9qkxdawL',
first: 'r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096',
proof: 'c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=',
final: 'v=rmF9pqV8S7suAoZWja4dJRkFsKQ='
},
{
source: 'https://www.rfc-editor.org/rfc/rfc7677.html#section-3',
mechanism: 'SCRAM-SHA-256', username: 'user', password: 'pencil',
nonce: 'rOprNGfwEbeRWgbNEkqO',
first: 'r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096',
proof: 'c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=',
final: 'v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4='
}
];
@@ -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,122 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {createServer} from 'node:https';
import {mkdtempSync, readFileSync, rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {execFileSync} from 'node:child_process';
import {once} from 'node:events';
import WS, {WebSocketServer} from 'ws';
import {Client, installBoundedTransport, validatedSasl, importMessage, validateMessage, backend} from '../dist/browser.mjs';
import {vectors as rfc} from './fixtures.mjs';
const sha512 = JSON.parse(readFileSync(new URL('./scramp-vectors.json', import.meta.url)));
const vectors = [...rfc, sha512[0]];
const enc = s => Buffer.from(s).toString('base64');
const sasl = (type, data='') => `<${type} xmlns="urn:ietf:params:xml:ns:xmpp-sasl">${enc(data)}</${type}>`;
const waitFor = async fn => {
for (let i=0;i<300;i++) { if (fn()) return; await new Promise(r=>setTimeout(r,10)); }
throw new Error('Fixture timeout');
};
const dir = mkdtempSync(join(tmpdir(), 'luma-wss-'));
// Ephemeral synthetic localhost CA/server certificate; never committed.
execFileSync('openssl', ['req','-x509','-newkey','rsa:2048','-nodes','-keyout',join(dir,'key.pem'),
'-out',join(dir,'cert.pem'),'-days','1','-subj','/CN=localhost',
'-addext','subjectAltName=DNS:localhost,IP:127.0.0.1'], {stdio:'ignore'});
const cert = readFileSync(join(dir,'cert.pem'));
const key = readFileSync(join(dir,'key.pem'));
test.after(()=>rmSync(dir,{recursive:true,force:true}));
async function scenario(t, vector, offers, mode='valid') {
const server=createServer({key,cert});
const wss=new WebSocketServer({server,handleProtocols: protocols=>protocols.has('xmpp')?'xmpp':false});
const seen={opens:0,success:0,failed:0,rejected:[],policies:[],peerErrors:[]};
const client=new Client({jid:'user@example.invalid',server:'example.invalid',autoReconnect:false,
credentials:{username:vector.username,password:vector.password,clientNonce:vector.nonce},
transports:{'bounded-websocket':'wss://127.0.0.1'},transportPreferenceOrder:['bounded-websocket']});
// Explicit CA trust for this fixture only; hostname/TLS validation stays enabled.
class TrustedSocket extends WS {constructor(url,protocol){super(url,protocol,{ca:cert,rejectUnauthorized:true});}}
installBoundedTransport(client,TrustedSocket);
client.use(validatedSasl);
client.on('auth:success',()=>seen.success++);
client.on('auth:failed',()=>seen.failed++);
client.on('ingress:rejected',code=>seen.rejected.push(code));
client.on('sasl:policy',policy=>seen.policies.push(policy));
wss.on('connection',(socket,req)=>{
seen.protocol=req.headers['sec-websocket-protocol'];
seen.tls=socket._socket.encrypted;
socket.on('message', (bytes,isBinary)=>{
try {
assert.equal(isBinary,false);
const xml=bytes.toString();
if (xml.startsWith('<open')) {
seen.opens++;
socket.send('<open xmlns="urn:ietf:params:xml:ns:xmpp-framing" from="example.invalid" version="1.0" id="fixture"/>');
if (seen.opens===1) {
if (mode==='oversize') return socket.send('<message xmlns="jabber:client"><body>'+ 'x'.repeat(4096)+'</body></message>');
if (mode==='split') return socket.send('<message xmlns="jabber:client">');
socket.send('<stream:features xmlns:stream="http://etherx.jabber.org/streams"><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">'+offers.map(x=>`<mechanism>${x}</mechanism>`).join('')+'</mechanisms></stream:features>');
}
} else if (xml.startsWith('<auth')) {
seen.mechanism=/mechanism="([^"]+)"/.exec(xml)?.[1];
const initial=Buffer.from(/>([^<]+)</.exec(xml)[1],'base64').toString();
assert.equal(initial,`n,,n=user,r=${vector.nonce}`);
socket.send(mode==='premature'?sasl('success',vector.final):sasl('challenge',vector.first));
} else if (xml.startsWith('<response')) {
seen.proof=Buffer.from(/>([^<]+)</.exec(xml)[1],'base64').toString();
assert.equal(seen.proof,vector.proof);
socket.send(sasl('success',mode==='forged'?'v='+ 'A'.repeat(vector.final.length-2):vector.final));
}
} catch(error) {seen.peerErrors.push(error.message); socket.close();}
});
});
server.listen(0,'127.0.0.1'); await once(server,'listening');
client.updateConfig({transports:{'bounded-websocket':`wss://127.0.0.1:${server.address().port}`}});
t.after(async()=>{await client.disconnect(); for(const socket of wss.clients)socket.terminate(); await new Promise(r=>wss.close(r)); await new Promise(r=>server.close(r));});
await client.connect();
await waitFor(()=> mode==='valid'? seen.opens===2 : seen.failed||seen.rejected.length||seen.peerErrors.length);
assert.deepEqual(seen.peerErrors,[]);
assert.equal(seen.protocol,'xmpp'); assert.equal(seen.tls,true);
if(mode==='valid') {
assert.equal(seen.success,1); assert.equal(seen.failed,0); assert.equal(seen.mechanism,vector.mechanism);
assert.equal(seen.proof,vector.proof); assert.equal(seen.opens,2);
assert.deepEqual(seen.policies,[{selected:vector.mechanism,channelBinding:'unavailable',fallback:'disabled'}]);
} else {assert.equal(seen.success,0);assert.equal(seen.opens,1);}
return seen;
}
test('browser bundle uses browser backend with TLS validation enabled',()=>{assert.equal(backend,'browser');assert.notEqual(process.env.NODE_TLS_REJECT_UNAUTHORIZED,'0');});
test('public transport rejects plaintext endpoint before socket creation',async()=>{
const client=new Client({transports:{'bounded-websocket':'ws://127.0.0.1'},transportPreferenceOrder:['bounded-websocket']});
let sockets=0; installBoundedTransport(client,class{constructor(){sockets++;}});
await assert.rejects(client.connect(),/WSS required/);assert.equal(sockets,0);
});
for(const vector of vectors) {
const offers=['PLAIN','SCRAM-SHA-512-PLUS','SCRAM-SHA-256-PLUS','SCRAM-SHA-1',
...(vector.mechanism!=='SCRAM-SHA-1'?['SCRAM-SHA-256']:[]),
...(vector.mechanism==='SCRAM-SHA-512'?['SCRAM-SHA-512']:[])];
test(`trusted WSS strongest compatible ${vector.mechanism}, verified restart`,t=>scenario(t,vector,offers));
for(const mode of ['forged','premature'])test(`WSS ${vector.mechanism} rejects ${mode}`,t=>scenario(t,vector,offers,mode));
}
for(const mode of ['oversize','split']) test(`WSS bounded ingress rejects ${mode}`,async t=>{
const seen=await scenario(t,vectors[2],['SCRAM-SHA-512'],mode);assert.equal(seen.rejected.length,1);
});
for(const offers of [['SCRAM-SHA-512-PLUS'],['PLAIN'],[]]) test(`WSS refuses unsupported offer ${offers}`,async t=>{
const seen=await scenario(t,vectors[2],offers,'unsupported'); assert.equal(seen.failed,1); assert.equal(seen.mechanism,undefined);
});
test('default native WebSocket rejects untrusted WSS certificate',async t=>{
const server=createServer({key,cert}); const wss=new WebSocketServer({server});
server.listen(0,'127.0.0.1');await once(server,'listening');
t.after(async()=>{wss.close();await new Promise(r=>server.close(r));});
const socket=new globalThis.WebSocket(`wss://127.0.0.1:${server.address().port}`,'xmpp');
let opened=false;socket.onopen=()=>{opened=true;socket.close();};
await new Promise((resolve,reject)=>{const timer=setTimeout(()=>{socket.close();reject(new Error('TLS timeout'));},3000);socket.onerror=()=>{clearTimeout(timer);resolve();};});
assert.equal(opened,false);
});
test('XML resource and standalone boundaries compose before registry import',()=>{
const client=new Client();let imports=0; const registry={getImportKey:()=>{imports++;return 'message';},import:()=>{imports++;return {};}};
for(const data of ['<message>', '<message/><message/>','<!DOCTYPE x><x/>','<x>'+ '<x>'.repeat(8)+'</x>'.repeat(8)+'</x>',new Uint8Array(1)]) {
assert.throws(()=>importMessage(data,registry,{}));assert.equal(imports,0);
}
assert.doesNotThrow(()=>validateMessage('<message xmlns="jabber:client"><body>safe &amp; synthetic</body></message>'));
assert.equal(importMessage('<message xmlns="jabber:client"><body>synthetic</body></message>',client.stanzas,{}).stanza.body,'synthetic');
});
@@ -0,0 +1,113 @@
// Selection experiment: reviewed guard with explicit strongest-first non-PLUS policy.
import {registerSha512} from './sha512-mechanism.mjs';
// No production transport, credential persistence, or channel binding is provided.
const SUPPORTED = new Set(['SCRAM-SHA-512', 'SCRAM-SHA-256', 'SCRAM-SHA-1']);
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);
const length = {'SCRAM-SHA-512': 88, 'SCRAM-SHA-256': 44, 'SCRAM-SHA-1': 28}[name];
if (!length) throw new Error('Unsupported mechanism');
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 offered = features.sasl.mechanisms;
const names = ['SCRAM-SHA-512', 'SCRAM-SHA-256', 'SCRAM-SHA-1'].filter(name => offered.includes(name));
client.emit('sasl:policy', {selected: names[0] ?? null, channelBinding: 'unavailable', fallback: 'disabled'});
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'});
}
}]}
}
});
+2 -2
View File
@@ -1,12 +1,12 @@
# Матрица задач
Generated; state revision **112**. Canonical status: state/tasks.json.
Generated; state revision **114**. Canonical status: state/tasks.json.
| ID | Этап | Оператор | Reviewer | Статус | Owner | Зависимости | Задача |
| --- | --- | --- | --- | --- | --- | --- | --- |
| [S00-001](tasks/S00-001.md) | S00 | orchestrator | security | done | auto-orchestrator-48aab142ecde | | Утвердить Web-профиль |
| [S00-002](tasks/S00-002.md) | S00 | orchestrator | security | done | auto-orchestrator-68a05c587e33 | S00-001, S00-011, S00-012 | Выбрать framework и build runtime |
| [S00-003](tasks/S00-003.md) | S00 | protocol | qa | todo | | S00-001, S00-002, S00-017, S00-018, S00-019, S00-020 | Проверить XMPP WSS/auth библиотеку |
| [S00-003](tasks/S00-003.md) | S00 | protocol | qa | review | auto-protocol-f474114f0168 | S00-001, S00-002, S00-017, S00-018, S00-019, S00-020 | Проверить XMPP WSS/auth библиотеку |
| [S00-004](tasks/S00-004.md) | S00 | crypto | security | blocked | auto-crypto-fe1c81c17ded | S00-001, S00-002, S00-021, S00-022 | Проверить browser OMEMO backend |
| [S00-005](tasks/S00-005.md) | S00 | storage | qa | todo | | S00-002, S00-023 | Выбрать IndexedDB wrapper и migration policy |
| [S00-006](tasks/S00-006.md) | S00 | orchestrator | security | todo | | S00-002 | Зафиксировать browser/reference matrix |
+1 -1
View File
@@ -8,7 +8,7 @@
- [S00-001](../tasks/S00-001.md): Утвердить Web-профиль — orchestrator; done.
- [S00-002](../tasks/S00-002.md): Выбрать framework и build runtime — orchestrator; done.
- [S00-003](../tasks/S00-003.md): Проверить XMPP WSS/auth библиотеку — protocol; todo.
- [S00-003](../tasks/S00-003.md): Проверить XMPP WSS/auth библиотеку — protocol; review.
- [S00-004](../tasks/S00-004.md): Проверить browser OMEMO backend — crypto; blocked.
- [S00-005](../tasks/S00-005.md): Выбрать IndexedDB wrapper и migration policy — storage; todo.
- [S00-006](../tasks/S00-006.md): Зафиксировать browser/reference matrix — orchestrator; todo.
+1 -1
View File
@@ -1,7 +1,7 @@
id,stage,operator,reviewer,status,owner,depends_on,title
S00-001,S00,orchestrator,security,done,auto-orchestrator-48aab142ecde,,Утвердить Web-профиль
S00-002,S00,orchestrator,security,done,auto-orchestrator-68a05c587e33,"S00-001, S00-011, S00-012",Выбрать framework и build runtime
S00-003,S00,protocol,qa,todo,,"S00-001, S00-002, S00-017, S00-018, S00-019, S00-020",Проверить XMPP WSS/auth библиотеку
S00-003,S00,protocol,qa,review,auto-protocol-f474114f0168,"S00-001, S00-002, S00-017, S00-018, S00-019, S00-020",Проверить XMPP WSS/auth библиотеку
S00-004,S00,crypto,security,blocked,auto-crypto-fe1c81c17ded,"S00-001, S00-002, S00-021, S00-022",Проверить browser OMEMO backend
S00-005,S00,storage,qa,todo,,"S00-002, S00-023",Выбрать IndexedDB wrapper и migration policy
S00-006,S00,orchestrator,security,todo,,S00-002,Зафиксировать browser/reference matrix
1 id stage operator reviewer status owner depends_on title
2 S00-001 S00 orchestrator security done auto-orchestrator-48aab142ecde Утвердить Web-профиль
3 S00-002 S00 orchestrator security done auto-orchestrator-68a05c587e33 S00-001, S00-011, S00-012 Выбрать framework и build runtime
4 S00-003 S00 protocol qa todo review auto-protocol-f474114f0168 S00-001, S00-002, S00-017, S00-018, S00-019, S00-020 Проверить XMPP WSS/auth библиотеку
5 S00-004 S00 crypto security blocked auto-crypto-fe1c81c17ded S00-001, S00-002, S00-021, S00-022 Проверить browser OMEMO backend
6 S00-005 S00 storage qa todo S00-002, S00-023 Выбрать IndexedDB wrapper и migration policy
7 S00-006 S00 orchestrator security todo S00-002 Зафиксировать browser/reference matrix
@@ -0,0 +1,4 @@
Evaluate Stanza with verified SCRAM and bounded WSS ingress
Changes: Propose the pinned XMPP library decision and add an isolated browser-built selection spike combining reviewed public SASL, SHA-512 and parser seams. Exercise TLS/xmpp handshake, strongest compatible non-PLUS authentication, rejected unsafe paths and extension mappings; document duplicate-body export projection and remaining limits. Preserve dependency notices, evidence and QA handoff.
Validation: Clean-cache pinned npm ci succeeded; npm test passed 112 checks; check-dependencies.py verified 51 locked entries, 26 installed packages and 340 unchanged vendor files; make check passed 72 tooling tests. No actual browser runtime, independent full XMPP server/client or production encryption/release readiness claim.
@@ -0,0 +1,129 @@
# S00-003 executor evidence
Agent: auto-protocol-f474114f0168 (protocol), 2026-10-15.
Result: proposed Stanza selection with combined runnable spike; independent QA pending.
Input: master/AGENTS/README/common/protocol; profile, architecture, contracts,
workflow, automation, Git, required TESTING and decisions registry; assigned
resume/show/card/context packet, previous blocked handoff; exact immutable
ACC-004/NET-003/NET-004; accepted scope/stack/SASL/parser/SHA512 ADRs, direct
prerequisite evidence and handoffs, remediation independent reviews including
S00-020; shipped vendor API/source. No missing input or unresolved blocker.
No repeat claim, agent launch, other task execution or Git mutation.
One output: proposed docs/decisions/0002-xmpp-library.md and isolated
experiments/0002-xmpp-library/selection-spike. Source scope unchanged elsewhere;
workflow evidence/handoff/message are metadata. Original parent manifest/lock,
accepted experiments/ADRs, shared contracts and spec snapshots unchanged.
Initial git status showed only coordinator state/tasks.json claim modification.
## Actual commands and results
All npm/node commands used PATH=/tmp/luma-S00-026-j1pmys7w/node-v24.21.0-linux-x64/bin:$PATH
from project root (authentic Linux runtime independently accepted by S00-020).
- python3 scripts/tasks.py resume --agent auto-protocol-f474114f0168 and
python3 scripts/tasks.py show S00-003: exit 0 each; assigned in_progress.
python3 scripts/tasks.py packet S00-003 --agent auto-protocol-f474114f0168:
exit 0, exact packet retrieved again before handoff.
- node --version / npm --version: exit 0 each, v24.21.0 / 11.19.0.
- npm install --prefix experiments/0002-xmpp-library/selection-spike
--ignore-scripts --no-audit --no-fund --strict-ssl=true --engine-strict=true
--fetch-retries=0 --fetch-timeout=15000 --cache /tmp/luma-selection-cache:
exit 0, 26 packages, independent lock generated. No upgrade notice acted on.
- Initial npm test: exit 1, 3/17 passed. Fixture incorrectly supplied endpoint
as {url:...}; shipped Client.connect requires string. Corrected only test
config; rerun exit 0, 17/17. This was a harness error, not missing WSS support.
- Extension test development npm test: exit 1, 21/23 passed. Guessed upload field
was wrong (actual public shape httpUpload.download); raw imported MAM export
exposed duplicated body projection. Inspected shipped mapping and diagnostic
import/export, corrected field assertion and made explicit fixture export
projection. Rerun exit 0, 23/23. Added permanent raw duplicate regression;
finding and future adapter obligation retained in ADR, not hidden as a pass.
- Copied unchanged reviewed SHA512 backend/feature-dispatcher regressions into
this independent package; npm test exit 0, 110 passed. No crypto reimplementation.
- npm ci --prefix experiments/0002-xmpp-library/selection-spike --ignore-scripts
--no-audit --no-fund --engine-strict=true --strict-ssl=true --fetch-retries=0
--fetch-timeout=15000 --cache /tmp/luma-S00-003-clean-f474114f0168:
exit 0, new empty cache, 26 installed packages. Lock retained; scripts disabled.
- Final npm test --prefix experiments/0002-xmpp-library/selection-spike
> /tmp/luma-S00-003-final-tests.log 2>&1: exit 0 after clean install;
build 171 modules, browser.mjs 695.84 kB / gzip 141.63 kB;
112 passed, zero failures/skips/cancellations, duration 3121.874019 ms.
Final two checks add plaintext-endpoint rejection and explicit raw-export
regression. No plugin/transport/crypto source changes after clean install.
- python3 experiments/0002-xmpp-library/selection-spike/check-dependencies.py:
exit 0 before and after clean install; 51 lock entries, 26 installed exact
pins/notices, HTTPS registry/SHA512 integrity, all 340 Stanza files match
independently installed environment-access vendor graph; reviewed parser,
SHA512 alias and saxes license byte-identical. Browser module graph verified.
- make check > /tmp/luma-S00-003-check.log 2>&1: exit 0; 296 tasks / 22 stages,
221 requirements, 211 allocated Web requirements; 72 tooling tests in 18.819s.
Foundation checks only, not application readiness.
- git diff --check: exit 0. Final source scope and authored whitespace checked
separately including untracked files; final result recorded below.
## Demonstrated result and evidence boundaries
Real loopback HTTPS + ws fixture peer, ephemeral OpenSSL localhost certificate,
explicit per-fixture CA trust and rejectUnauthorized:true. Actual TLS hostname
validation remains; native WebSocket without fixture CA rejects that certificate.
No system trust/network/proxy change, disabled TLS, CORS bypass or real provider.
Plain ws endpoint rejects before socket construction. Private keys exist only
in temporary test directory and are deleted by test cleanup; no committed key.
Tests import actual browser-built Client, Registry, SCRAM and bounded transport.
Client.connect/queue/feature dispatcher run unchanged. Peer observes TLS and xmpp
subprotocol, framing open, exact fixed proof and restart only after verified
final. Mixed offers choose SHA512, then SHA256, then SHA1 as available. Public
RFC SHA1/256 and independently accepted Scramp SHA512 transcript fixtures are
compared exactly. Forged/premature final never succeeds or restarts. PLUS-only,
PLAIN-only and empty offers fail; diagnostics report binding unavailable and
fallback disabled. Existing regression rejects synthetic tlsUnique, verifies
n,,/c=biws, reset/order/send races and lack of persisted derived cache.
WSS oversized/incomplete messages close before any stanza delivery; pre-import
checks also reject depth, multiple roots, DTD and binary types. Full resource
and stale-callback evidence remains in byte-identical accepted parser source and
S00-018 tests; those standalone suites were not separately rerun here.
Registry import/export fixtures preserve MAM IDs, MUC join, Jingle session ID,
upload URL, legacy opaque bytes and invented plugin namespace isolation.
Raw dual-body export duplication is deliberately reproduced, with explicit
single-representation export for the MAM fixture. This is an API mapping limit
for future adapters, not an unreviewed shared-interface change.
## Digests and limitations
SHA256:
- package-lock.json: 02bb48038b194b72efc688752927815e3de94bf589bfe1d2e5bcee68283c4abc
- validated-sasl.mjs: 42a7e751565c27b45eee371852973d3613d26981586b48271d78586141466d61
- dist/browser.mjs: b95b0ba5c2fbf5d52b5b1e62c40f6b4d5669d22499cbfef0fe3d88b6bf708cee
Primary sources opened through web tools on 2026-10-15: RFC7395, RFC5802,
Stanza and xmpp.js upstream, XEP0313/0045/0166/0363. Exact shipped pinned source
supplies authoritative API/version behavior. Candidate comparison is documentary
for xmpp.js, not installed/tested equivalence. Dated source links are in ADR.
License inventory includes optional hosts; installed notices include Stanza
ancestry, preserved saxes upstream license and unmodified MPL build dependencies.
No advisory/provenance clearance claimed.
No actual browser runtime, independent full XMPP server/client, OMEMO encryption,
OMEMO2 wire validation, product support matrix, load/timing/security audit or
release readiness. Browser artifact executes in Node with native host APIs and
an injected trusted fixture socket; no browser channel binding is available.
Strict SCRAM profile, no timeout, synchronous derivation/string verifier equality,
local ingress limits, incomplete stream lifecycle and unbounded aggregate queues
remain disclosed in ADR. Public extension seam is verified, future API stability
is not promised. No real accounts/conversations/secrets or fake crypto used.
QA should inspect source/diff/message, repeat README with fresh cache and pinned
runtime, verify dependency graph/notices/unchanged vendor and limitations, then
record an independent review decision. Executor submits and stops; coordinator
commits before review. Proposed ADR is not independently accepted yet.
Final verification: first inline whitespace assertion failed on verbatim
THIRD_PARTY_NOTICES.md upstream trailing spaces; it was not an implementation
failure and notices were preserved unchanged. Corrected inspection exempts
verbatim notices/license texts: exit 0, all changed paths within task scope or
allowed metadata, authored final-newline/trailing-whitespace checks passed.
git diff --check exited 0. No failed check is described as passed.
+33 -22
View File
@@ -1,27 +1,38 @@
# S00-003 blocked handoff
# S00-003 handoff for independent QA review
Executor: auto-protocol-59f4d359b339, protocol, 2026-10-15.
Outcome remains library selection with WSS/auth/parser/extension spike.
Executor: auto-protocol-f474114f0168 (protocol), 2026-10-15.
No blocker. Proposed Stanza 12.22.1 selection and combined isolated spike at
experiments/0002-xmpp-library/selection-spike; ADR docs/decisions/0002-xmpp-library.md.
Accepted prerequisites S00-001/002/017/018/019 were read. The SHA-512 public
mechanism composition is now independently accepted; the earlier crypto blocker
is resolved. Their suites were not rerun in this environment. Parent candidate
remains unselected and selection ADR absent.
Actual result: clean-cache locked install on accepted Linux Node24.21.0/npm11.19.0,
browser bundle and 112 tests pass. Real synthetic loopback WSS TLS/xmpp handshake,
fixed SCRAM512/256/1 proofs/finals/restart, strongest compatible non-PLUS policy,
PLUS/plaintext rejection, untrusted certificate/plain endpoint rejection,
bounded XML over WSS and registry extension checks. 51 locked/26 installed
package checks and notices, 340 unmodified vendor files, reviewed parser/SHA512
source reuse verified. make check: 72 tooling tests passed, exit 0.
Evidence: state/evidence/S00-003-auto-protocol-f474114f0168.md.
Current blocker: accepted Node 24.21.0/npm 11.19.0 and installed Stanza are not
available in this Linux session (host tools 24.1.0/11.3.0; parent node_modules
absent). Official checksum retrieval over configured HTTPS fails with curl exit
5, unresolved proxy. See state/evidence/S00-003-auto-protocol-59f4d359b339.md for exact reproduction.
QA next: inspect actual diff/executor message, proposed ADR and README. Repeat
clean npm ci with a new cache, npm test, check-dependencies.py and relevant
foundation check using reviewed runtime currently at
/tmp/luma-S00-026-j1pmys7w/node-v24.21.0-linux-x64/bin.
Temporary tools may expire; environment-access probe can recreate authentic
runtime/vendor comparison graph. Do not change accepted pins or security policy.
Verify browser graph, public seams, fail-closed effects, notices and limitations.
Next input: external environment owner must restore approved HTTPS access to
nodejs.org and registry.npmjs.org or provide authentic, checksum/integrity-
verifiable pinned Linux toolchain and locked package artifacts through an
approved local path. Do not change stack pins, bypass proxy policy/TLS, fabricate
engine metadata or reuse a macOS binary. Fresh remediation executor must verify
exact runtime and lock installation, with independent review before parent
returns to a new executor. Supply no credentials in committed evidence.
Raw imported messages carry selected body plus language bodies; passing both to
export duplicates text in this version. Permanent regression records it. MAM
fixture explicitly exports the language collection only; future adapters must
construct outbound shapes. Upload's actual field is httpUpload.download.
Initial harness failures and corrections are recorded, not counted as passed.
No source changes or installs/build/tests/browser/WSS execution occurred. Browser
binaries are present but not exercised. No make check, complete SASL policy,
extension roundtrip, independent interoperability or selection claimed.
Reporter exits after escalation; no resolver work, unblock or new agent launch.
Limits: browser-built backend runs in Node, no actual browser or independent
full XMPP server/client; scripted peer uses accepted fixed transcripts. No OMEMO
crypto/OMEMO2 wire validation, full XEP semantics or release readiness. Fixture
certificate trusted only through injected socket with validation enabled; no
system trust changes/keys committed. No real binding/fallback. Inherited strict
SCRAM layout/timeout/timing, minimal transport/lifecycle and aggregate queue
limits remain. Source changes are only current task paths plus workflow metadata.
ADR stays proposed until separate QA acceptance. Coordinator alone commits and
launches new process; executor exits immediately after submit.
+30 -6
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"revision": 112,
"revision": 114,
"tasks": {
"S00-001": {
"status": "done",
@@ -32,17 +32,18 @@
"completed_at": "2026-10-14T19:06:23+00:00"
},
"S00-003": {
"status": "todo",
"owner": null,
"started_at": "2026-10-15T04:12:38+00:00",
"status": "review",
"owner": "auto-protocol-f474114f0168",
"started_at": "2026-10-15T11:10:08+00:00",
"reason": null,
"updated_at": "2026-10-15T10:09:24+00:00",
"updated_at": "2026-10-15T11:19:59+00:00",
"blocked_by": [],
"resume_status": "in_progress",
"blocker_report": "state/blockers/S00-020.md",
"handoff": "state/handoffs/S00-003.md",
"previous_owner": "auto-protocol-59f4d359b339",
"resumed_after_blocker": true
"resumed_after_blocker": true,
"evidence": "state/evidence/S00-003-auto-protocol-f474114f0168.md"
},
"S00-004": {
"status": "blocked",
@@ -2901,6 +2902,29 @@
"to": "done",
"reason": null,
"evidence": "state/evidence/S00-023-review.md"
},
{
"revision": 113,
"at": "2026-10-15T11:10:08+00:00",
"action": "claim",
"task": "S00-003",
"agent": "auto-protocol-f474114f0168",
"role": "protocol",
"from": "todo",
"to": "in_progress",
"reason": null
},
{
"revision": 114,
"at": "2026-10-15T11:19:59+00:00",
"action": "submit",
"task": "S00-003",
"agent": "auto-protocol-f474114f0168",
"role": null,
"from": "in_progress",
"to": "review",
"reason": null,
"evidence": "state/evidence/S00-003-auto-protocol-f474114f0168.md"
}
],
"retired_agents": [