diff --git a/eslint.config.js b/eslint.config.js
index 88a10dee..73236769 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -53,6 +53,7 @@ export default [
],
"prefer-arrow-callback": ["error", { allowNamedFunctions: true }],
"no-redeclare": ["error", { builtinGlobals: false }],
+ "no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
// node
// https://github.com/eslint-community/eslint-plugin-n/
diff --git a/packages/client/browser.js b/packages/client/browser.js
index 3812fb45..b77c440f 100644
--- a/packages/client/browser.js
+++ b/packages/client/browser.js
@@ -1,5 +1,6 @@
import { xml, jid, Client } from "@xmpp/client-core";
import getDomain from "./lib/getDomain.js";
+import createOnAuthenticate from "./lib/createOnAuthenticate.js";
import _reconnect from "@xmpp/reconnect";
import _websocket from "@xmpp/websocket";
@@ -14,6 +15,7 @@ import _resourceBinding from "@xmpp/resource-binding";
import _sessionEstablishment from "@xmpp/session-establishment";
import _streamManagement from "@xmpp/stream-management";
+import SASLFactory from "saslmechanisms";
import plain from "@xmpp/sasl-plain";
import anonymous from "@xmpp/sasl-anonymous";
@@ -35,8 +37,19 @@ function client(options = {}) {
const iqCaller = _iqCaller({ middleware, entity });
const iqCallee = _iqCallee({ middleware, entity });
const resolve = _resolve({ entity });
+
+ // SASL mechanisms - order matters and define priority
+ const saslFactory = new SASLFactory();
+ const mechanisms = Object.entries({
+ plain,
+ anonymous,
+ }).map(([k, v]) => ({ [k]: v(saslFactory) }));
+
// Stream features - order matters and define priority
- const sasl = _sasl({ streamFeatures }, credentials || { username, password });
+ const sasl = _sasl(
+ { streamFeatures, saslFactory },
+ createOnAuthenticate(credentials ?? { username, password }),
+ );
const streamManagement = _streamManagement({
streamFeatures,
entity,
@@ -50,10 +63,6 @@ function client(options = {}) {
iqCaller,
streamFeatures,
});
- // SASL mechanisms - order matters and define priority
- const mechanisms = Object.entries({ plain, anonymous }).map(([k, v]) => ({
- [k]: v(sasl),
- }));
return Object.assign(entity, {
entity,
@@ -69,6 +78,7 @@ function client(options = {}) {
sessionEstablishment,
streamManagement,
mechanisms,
+ saslFactory,
});
}
diff --git a/packages/client/index.js b/packages/client/index.js
index 36dbc494..24ac2a1e 100644
--- a/packages/client/index.js
+++ b/packages/client/index.js
@@ -1,5 +1,6 @@
import { xml, jid, Client } from "@xmpp/client-core";
import getDomain from "./lib/getDomain.js";
+import createOnAuthenticate from "./lib/createOnAuthenticate.js";
import _reconnect from "@xmpp/reconnect";
import _websocket from "@xmpp/websocket";
@@ -11,7 +12,7 @@ import _iqCaller from "@xmpp/iq/caller.js";
import _iqCallee from "@xmpp/iq/callee.js";
import _resolve from "@xmpp/resolve";
-import _starttls from "@xmpp/starttls/client.js";
+import _starttls from "@xmpp/starttls";
import _sasl from "@xmpp/sasl";
import _resourceBinding from "@xmpp/resource-binding";
import _sessionEstablishment from "@xmpp/session-establishment";
@@ -55,7 +56,7 @@ function client(options = {}) {
const starttls = _starttls({ streamFeatures });
const sasl = _sasl(
{ streamFeatures, saslFactory },
- credentials || { username, password },
+ createOnAuthenticate(credentials ?? { username, password }),
);
const streamManagement = _streamManagement({
streamFeatures,
diff --git a/packages/client/lib/createOnAuthenticate.js b/packages/client/lib/createOnAuthenticate.js
new file mode 100644
index 00000000..d8b142f3
--- /dev/null
+++ b/packages/client/lib/createOnAuthenticate.js
@@ -0,0 +1,21 @@
+const ANONYMOUS = "ANONYMOUS";
+
+export default function createOnAuthenticate(credentials) {
+ return async function onAuthenticate(authenticate, mechanisms) {
+ if (typeof credentials === "function") {
+ await credentials(authenticate, mechanisms);
+ return;
+ }
+
+ if (
+ !credentials?.username &&
+ !credentials?.password &&
+ mechanisms.includes(ANONYMOUS)
+ ) {
+ await authenticate(credentials, ANONYMOUS);
+ return;
+ }
+
+ await authenticate(credentials, mechanisms[0]);
+ };
+}
diff --git a/packages/client/test/getDomain.js b/packages/client/lib/getDomain.test.js
similarity index 100%
rename from packages/client/test/getDomain.js
rename to packages/client/lib/getDomain.test.js
diff --git a/packages/client/package.json b/packages/client/package.json
index e63da41f..4bf39f21 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -27,8 +27,8 @@
"@xmpp/websocket": "^0.13.2",
"saslmechanisms": "^0.1.1"
},
- "browser": "./browser.js",
- "react-native": "./react-native.js",
+ "browser": "browser.js",
+ "react-native": "browser.js",
"engines": {
"node": ">= 20"
},
diff --git a/packages/client/react-native.js b/packages/client/react-native.js
deleted file mode 100644
index 3812fb45..00000000
--- a/packages/client/react-native.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import { xml, jid, Client } from "@xmpp/client-core";
-import getDomain from "./lib/getDomain.js";
-
-import _reconnect from "@xmpp/reconnect";
-import _websocket from "@xmpp/websocket";
-import _middleware from "@xmpp/middleware";
-import _streamFeatures from "@xmpp/stream-features";
-import _iqCaller from "@xmpp/iq/caller.js";
-import _iqCallee from "@xmpp/iq/callee.js";
-import _resolve from "@xmpp/resolve";
-
-import _sasl from "@xmpp/sasl";
-import _resourceBinding from "@xmpp/resource-binding";
-import _sessionEstablishment from "@xmpp/session-establishment";
-import _streamManagement from "@xmpp/stream-management";
-
-import plain from "@xmpp/sasl-plain";
-import anonymous from "@xmpp/sasl-anonymous";
-
-function client(options = {}) {
- const { resource, credentials, username, password, ...params } = options;
-
- const { domain, service } = params;
- if (!domain && service) {
- params.domain = getDomain(service);
- }
-
- const entity = new Client(params);
-
- const reconnect = _reconnect({ entity });
- const websocket = _websocket({ entity });
-
- const middleware = _middleware({ entity });
- const streamFeatures = _streamFeatures({ middleware });
- const iqCaller = _iqCaller({ middleware, entity });
- const iqCallee = _iqCallee({ middleware, entity });
- const resolve = _resolve({ entity });
- // Stream features - order matters and define priority
- const sasl = _sasl({ streamFeatures }, credentials || { username, password });
- const streamManagement = _streamManagement({
- streamFeatures,
- entity,
- middleware,
- });
- const resourceBinding = _resourceBinding(
- { iqCaller, streamFeatures },
- resource,
- );
- const sessionEstablishment = _sessionEstablishment({
- iqCaller,
- streamFeatures,
- });
- // SASL mechanisms - order matters and define priority
- const mechanisms = Object.entries({ plain, anonymous }).map(([k, v]) => ({
- [k]: v(sasl),
- }));
-
- return Object.assign(entity, {
- entity,
- reconnect,
- websocket,
- middleware,
- streamFeatures,
- iqCaller,
- iqCallee,
- resolve,
- sasl,
- resourceBinding,
- sessionEstablishment,
- streamManagement,
- mechanisms,
- });
-}
-
-export { xml, jid, client };
diff --git a/packages/sasl/README.md b/packages/sasl/README.md
index c29bccd2..41a5a6de 100644
--- a/packages/sasl/README.md
+++ b/packages/sasl/README.md
@@ -29,22 +29,22 @@ Uses cases:
- Do not ask for password before connection is made
- Debug authentication
- Using a SASL mechanism with specific requirements
-- Perform an asynchronous operation to get credentials
+- Fetch credentials from a secure database
```js
import { xmpp } from "@xmpp/client";
-const client = xmpp({ credentials: authenticate });
+const client = xmpp({ credentials: onAuthenticate });
-async function authenticate(auth, mechanism) {
- console.debug("authenticate", mechanism);
+async function onAuthenticate(authenticate, mechanisms) {
+ console.debug("authenticate", mechanisms);
const credentials = {
username: await prompt("enter username"),
password: await prompt("enter password"),
};
console.debug("authenticating");
try {
- await auth(credentials);
+ await authenticate(credentials, mechanisms[0]);
console.debug("authenticated");
} catch (err) {
console.error(err);
diff --git a/packages/sasl/index.js b/packages/sasl/index.js
index cbbfc37f..8a64e38c 100644
--- a/packages/sasl/index.js
+++ b/packages/sasl/index.js
@@ -13,10 +13,10 @@ function getMechanismNames(features) {
.map((el) => el.text());
}
-async function authenticate(SASL, entity, mechname, credentials) {
- const mech = SASL.create([mechname]);
+async function authenticate({ saslFactory, entity, mechanism, credentials }) {
+ const mech = saslFactory.create([mechanism]);
if (!mech) {
- throw new Error("No compatible mechanism");
+ throw new Error(`SASL: Mechanism ${mechanism} not found.`);
}
const { domain } = entity.options;
@@ -73,29 +73,27 @@ async function authenticate(SASL, entity, mechname, credentials) {
});
}
-export default function sasl({ streamFeatures, saslFactory }, credentials) {
+export default function sasl({ streamFeatures, saslFactory }, onAuthenticate) {
streamFeatures.use("mechanisms", NS, async ({ stanza, entity }) => {
const offered = getMechanismNames(stanza);
const supported = saslFactory._mechs.map(({ name }) => name);
- // eslint-disable-next-line unicorn/prefer-array-find
- const intersection = supported.filter((mech) => {
- return offered.includes(mech);
- });
- let mech = intersection[0];
-
- if (typeof credentials === "function") {
- await credentials(
- (creds) => authenticate(saslFactory, entity, mech, creds, stanza),
- mech,
- );
- } else {
- if (!credentials.username && !credentials.password) {
- mech = "ANONYMOUS";
- }
+ const intersection = supported.filter((mech) => offered.includes(mech));
- await authenticate(saslFactory, entity, mech, credentials, stanza);
+ if (intersection.length === 0) {
+ throw new SASLError("SASL: No compatible mechanism available.");
}
+ async function done(credentials, mechanism) {
+ await authenticate({
+ saslFactory,
+ entity,
+ mechanism,
+ credentials,
+ });
+ }
+
+ await onAuthenticate(done, intersection);
+
await entity.restart();
});
}
diff --git a/packages/sasl/test.js b/packages/sasl/test.js
index 2521ada1..4b8a32de 100644
--- a/packages/sasl/test.js
+++ b/packages/sasl/test.js
@@ -5,7 +5,7 @@ const username = "foo";
const password = "bar";
const credentials = { username, password };
-test("no compatibles mechanisms", async () => {
+test("No compatible mechanism available", async () => {
const { entity } = mockClient({ username, password });
entity.mockInput(
@@ -18,7 +18,7 @@ test("no compatibles mechanisms", async () => {
const error = await promise(entity, "error");
expect(error instanceof Error).toBe(true);
- expect(error.message).toBe("No compatible mechanism");
+ expect(error.message).toBe("SASL: No compatible mechanism available.");
});
test("with object credentials", async () => {
@@ -48,14 +48,16 @@ test("with object credentials", async () => {
});
test("with function credentials", async () => {
+ expect.assertions(2);
+
const mech = "PLAIN";
- function authenticate(auth, mechanism) {
- expect(mechanism).toBe(mech);
- return auth(credentials);
+ async function onAuthenticate(authenticate, mechanisms) {
+ expect(mechanisms).toEqual([mech]);
+ await authenticate(credentials, mech);
}
- const { entity } = mockClient({ credentials: authenticate });
+ const { entity } = mockClient({ credentials: onAuthenticate });
entity.restart = () => {
entity.emit("open");
return Promise.resolve();
@@ -80,6 +82,53 @@ test("with function credentials", async () => {
await promise(entity, "online");
});
+test("Mechanism not found", async () => {
+ const { entity } = mockClient({
+ async credentials(authenticate, _mechanisms) {
+ await authenticate({ username, password }, "foo");
+ },
+ });
+
+ entity.mockInput(
+
+
+ FOO
+
+ ,
+ );
+
+ const error = await promise(entity, "error");
+ expect(error instanceof Error).toBe(true);
+ expect(error.message).toBe("SASL: No compatible mechanism available.");
+});
+
+test("with function credentials that rejects", (done) => {
+ expect.assertions(1);
+
+ const mech = "PLAIN";
+
+ const error = {};
+
+ async function onAuthenticate() {
+ throw error;
+ }
+
+ const { entity } = mockClient({ credentials: onAuthenticate });
+
+ entity.entity.mockInput(
+
+
+ {mech}
+
+ ,
+ );
+
+ entity.on("error", (err) => {
+ expect(err).toBe(error);
+ done();
+ });
+});
+
test("failure", async () => {
const { entity } = mockClient({ credentials });
@@ -135,9 +184,9 @@ test("use ANONYMOUS if username and password are not provided", async () => {
entity.mockInput(
- ANONYMOUS
PLAIN
SCRAM-SHA-1
+ ANONYMOUS
,
);
diff --git a/packages/starttls/client.js b/packages/starttls/index.js
similarity index 100%
rename from packages/starttls/client.js
rename to packages/starttls/index.js