diff --git a/eslint.config.js b/eslint.config.js
index 73236769..a750d61f 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -109,6 +109,7 @@ export default [
// },
// ],
"promise/no-callback-in-promise": "off",
+ "n/no-extraneous-import": ["error", { resolvePaths: ["packages"] }],
},
},
];
diff --git a/packages/client-core/src/fast/README.md b/packages/client-core/src/fast/README.md
index 8940b9a3..48c23d5c 100644
--- a/packages/client-core/src/fast/README.md
+++ b/packages/client-core/src/fast/README.md
@@ -6,7 +6,7 @@ By default `@xmpp/fast` stores the token in memory and as such fast authenticati
You can supply your own functions to store and retrieve the token from a persistent database.
-If fast authentication fails, regular authentication with `credentials` will happen.
+If fast authentication fails, regular authentication will happen.
## Usage
@@ -26,10 +26,9 @@ client.fast.saveToken = async (token) => {
await secureStorage.set("token", JSON.stringify(token));
}
-// Debugging only
-client.fast.on("error", (error) => {
- console.log("fast error", error);
-})
+client.fast.removeToken = async () => {
+ await secureStorage.del("token");
+}
```
## References
diff --git a/packages/client-core/src/fast/fast.js b/packages/client-core/src/fast/fast.js
index 6fb629d1..f48c9364 100644
--- a/packages/client-core/src/fast/fast.js
+++ b/packages/client-core/src/fast/fast.js
@@ -1,5 +1,6 @@
import { EventEmitter } from "@xmpp/events";
import { getAvailableMechanisms } from "@xmpp/sasl";
+import SASLError from "@xmpp/sasl/lib/SASLError.js";
import xml from "@xmpp/xml";
import SASLFactory from "saslmechanisms";
@@ -20,6 +21,9 @@ export default function fast({ sasl2, entity }) {
async fetchToken() {
return token;
},
+ async deleteToken() {
+ token = null;
+ },
async save(token) {
try {
await this.saveToken(token);
@@ -34,6 +38,13 @@ export default function fast({ sasl2, entity }) {
entity.emit("error", err);
}
},
+ async delete() {
+ try {
+ await this.deleteToken();
+ } catch (err) {
+ entity.emit("error", err);
+ }
+ },
saslFactory,
async auth({
authenticate,
@@ -68,6 +79,13 @@ export default function fast({ sasl2, entity }) {
});
return true;
} catch (err) {
+ if (
+ err instanceof SASLError &&
+ ["not-authorized", "credentials-expired"].includes(err.condition)
+ ) {
+ this.delete();
+ return false;
+ }
entity.emit("error", err);
return false;
}
diff --git a/packages/client-core/src/fast/isTokenValid.test.js b/packages/client-core/src/fast/isTokenValid.test.js
index dc20d811..61220040 100644
--- a/packages/client-core/src/fast/isTokenValid.test.js
+++ b/packages/client-core/src/fast/isTokenValid.test.js
@@ -1,5 +1,4 @@
import { isTokenValid } from "./fast.js";
-// eslint-disable-next-line n/no-extraneous-import
import { datetime } from "@xmpp/time";
const tomorrow = new Date();
diff --git a/packages/client-core/test/fast.js b/packages/client-core/test/fast.js
new file mode 100644
index 00000000..7ad4cc5b
--- /dev/null
+++ b/packages/client-core/test/fast.js
@@ -0,0 +1,118 @@
+import { tick } from "@xmpp/events";
+import { mockClient } from "@xmpp/test";
+import { datetime } from "@xmpp/time";
+import { Element } from "@xmpp/xml";
+
+const mechanism = "HT-SHA-256-NONE";
+
+test("requests and saves token if server advertises fast", async () => {
+ const { entity, fast } = mockClient();
+
+ const spy_saveToken = jest.spyOn(fast, "saveToken");
+
+ entity.mockInput(
+
+
+ PLAIN
+
+
+ {mechanism}
+
+
+
+ ,
+ );
+
+ const authenticate = await entity.catchOutgoing();
+ expect(authenticate.is("authenticate", "urn:xmpp:sasl:2")).toBe(true);
+ const request_token = authenticate.getChild(
+ "request-token",
+ "urn:xmpp:fast:0",
+ );
+ expect(request_token.attrs.mechanism).toBe(mechanism);
+
+ const token = "secret-token:fast-HZzFpFwHTy4nc3C8Y1NVNZqYef_7Q3YjMLu2";
+ const expiry = "2025-02-06T09:58:40.774329Z";
+
+ expect(spy_saveToken).not.toHaveBeenCalled();
+
+ entity.mockInput(
+
+
+
+ username@localhost/rOYwkWIywtnF
+
+ ,
+ );
+
+ expect(spy_saveToken).toHaveBeenCalledWith({ token, expiry, mechanism });
+});
+
+async function setupFast() {
+ const { entity, fast } = mockClient();
+
+ const d = new Date();
+ d.setFullYear(d.getFullYear() + 1);
+ const expiry = datetime(d);
+
+ fast.fetchToken = async () => {
+ return {
+ mechanism,
+ expiry,
+ token: "foobar",
+ };
+ };
+
+ entity.mockInput(
+
+
+ PLAIN
+
+
+ {mechanism}
+
+
+
+ ,
+ );
+
+ expect(fast.mechanism).toBe(mechanism);
+
+ const authenticate = await entity.catchOutgoing();
+ expect(authenticate.is("authenticate", "urn:xmpp:sasl:2"));
+ expect(authenticate.attrs.mechanism).toBe(mechanism);
+ expect(authenticate.getChild("fast", "urn:xmpp:fast:0")).toBeInstanceOf(
+ Element,
+ );
+
+ return entity;
+}
+
+test("deletes the token if server replies with not-authorized", async () => {
+ const entity = await setupFast();
+ const spy_deleteToken = jest.spyOn(entity.fast, "deleteToken");
+
+ expect(spy_deleteToken).not.toHaveBeenCalled();
+ entity.mockInput(
+
+
+ ,
+ );
+ await tick();
+ expect(spy_deleteToken).toHaveBeenCalled();
+});
+
+test("deletes the token if server replies with credentials-expired", async () => {
+ const entity = await setupFast();
+ const spy_deleteToken = jest.spyOn(entity.fast, "deleteToken");
+
+ // credentials-expired
+ expect(spy_deleteToken).not.toHaveBeenCalled();
+ entity.mockInput(
+
+
+ ,
+ );
+ await tick();
+ expect(spy_deleteToken).toHaveBeenCalled();
+});
diff --git a/packages/error/test.js b/packages/error/test.js
index 5905200d..af5c26a3 100644
--- a/packages/error/test.js
+++ b/packages/error/test.js
@@ -1,5 +1,4 @@
import XMPPError from "./index.js";
-// eslint-disable-next-line n/no-extraneous-import
import parse from "@xmpp/xml/lib/parse.js";
test("fromElement", () => {
diff --git a/packages/events/index.js b/packages/events/index.js
index cbe287f7..56c64636 100644
--- a/packages/events/index.js
+++ b/packages/events/index.js
@@ -9,6 +9,12 @@ import procedure from "./lib/procedure.js";
import listeners from "./lib/listeners.js";
import onoff from "./lib/onoff.js";
+function tick() {
+ return new Promise((resolve) => {
+ process.nextTick(resolve);
+ });
+}
+
export {
EventEmitter,
timeout,
@@ -19,4 +25,5 @@ export {
procedure,
listeners,
onoff,
+ tick,
};
diff --git a/packages/reconnect/test.js b/packages/reconnect/test.js
index 6ace155d..76933e14 100644
--- a/packages/reconnect/test.js
+++ b/packages/reconnect/test.js
@@ -1,5 +1,4 @@
import _reconnect from "./index.js";
-// eslint-disable-next-line n/no-extraneous-import
import Connection from "@xmpp/connection";
test("schedules a reconnect when disconnect is emitted", () => {
diff --git a/packages/starttls/starttls.test.js b/packages/starttls/starttls.test.js
index 3cad7750..c2f19d1a 100644
--- a/packages/starttls/starttls.test.js
+++ b/packages/starttls/starttls.test.js
@@ -1,7 +1,6 @@
import tls from "tls";
import { canUpgrade } from "./starttls.js";
import net from "net";
-// eslint-disable-next-line n/no-extraneous-import
import WebSocket from "@xmpp/websocket/lib/Socket.js";
test("canUpgrade", () => {
diff --git a/packages/stream-management/stream-features.test.js b/packages/stream-management/stream-features.test.js
index 4728a1a1..69f2fa64 100644
--- a/packages/stream-management/stream-features.test.js
+++ b/packages/stream-management/stream-features.test.js
@@ -1,10 +1,6 @@
import { mockClient } from "@xmpp/test";
-function tick() {
- return new Promise((resolve) => {
- process.nextTick(resolve);
- });
-}
+import { tick } from "@xmpp/events";
test("enable - enabled", async () => {
const { entity } = mockClient();
diff --git a/packages/tls/test.js b/packages/tls/test.js
index 82097918..bbfbba14 100644
--- a/packages/tls/test.js
+++ b/packages/tls/test.js
@@ -2,7 +2,6 @@ import ConnectionTLS from "./lib/Connection.js";
import tls from "tls";
import { promise } from "@xmpp/test";
-// eslint-disable-next-line n/no-extraneous-import
import selfsigned from "selfsigned";
test("socketParameters()", () => {
diff --git a/test/client.test.js b/test/client.test.js
index 457f180f..6437b653 100644
--- a/test/client.test.js
+++ b/test/client.test.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line n/no-extraneous-import
import { promise } from "@xmpp/events";
import { client, xml, jid } from "../packages/client/index.js";
import debug from "../packages/debug/index.js";