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:
1 parent
e08c0ab590
commit
51a414d386
28 files changed
+6709
-32
No files matched your search
@@ -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 & 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'});
|
||||
}
|
||||
}]}
|
||||
}
|
||||
});
|
||||
@@ -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 |
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
@@ -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
@@ -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
@@ -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": [
|
||||
|
||||
Reference in new issue
Block a user