diff --git a/.changeset/angry-poems-dream.md b/.changeset/angry-poems-dream.md new file mode 100644 index 000000000..8569f195a --- /dev/null +++ b/.changeset/angry-poems-dream.md @@ -0,0 +1,6 @@ +--- +"@xmtp/browser-sdk": patch +"@xmtp/node-sdk": patch +--- + +Add support for revoking specific installations diff --git a/sdks/browser-sdk/package.json b/sdks/browser-sdk/package.json index bdecfe093..aa9f8ff5d 100644 --- a/sdks/browser-sdk/package.json +++ b/sdks/browser-sdk/package.json @@ -64,7 +64,7 @@ "@xmtp/content-type-primitives": "^2.0.0", "@xmtp/content-type-text": "^2.0.0", "@xmtp/proto": "^3.72.3", - "@xmtp/wasm-bindings": "^0.0.9", + "@xmtp/wasm-bindings": "^0.0.11", "uuid": "^11.0.3" }, "devDependencies": { diff --git a/sdks/browser-sdk/src/Client.ts b/sdks/browser-sdk/src/Client.ts index 2d816220c..28c24df1f 100644 --- a/sdks/browser-sdk/src/Client.ts +++ b/sdks/browser-sdk/src/Client.ts @@ -126,8 +126,17 @@ export class Client extends ClientWorkerClass { return this.sendMessage("removeAccountSignatureText", { accountAddress }); } - async #revokeInstallationsSignatureText() { - return this.sendMessage("revokeInstallationsSignatureText", undefined); + async #revokeAllOtherInstallationsSignatureText() { + return this.sendMessage( + "revokeAllOtherInstallationsSignatureText", + undefined, + ); + } + + async #revokeInstallationsSignatureText(installationIds: Uint8Array[]) { + return this.sendMessage("revokeInstallationsSignatureText", { + installationIds, + }); } async #addSignature( @@ -208,8 +217,28 @@ export class Client extends ClientWorkerClass { await this.#applySignatures(); } - async revokeInstallations() { - const signatureText = await this.#revokeInstallationsSignatureText(); + async revokeAllOtherInstallations() { + const signatureText = + await this.#revokeAllOtherInstallationsSignatureText(); + + if (!signatureText) { + throw new Error( + "Unable to generate revoke all other installations signature text", + ); + } + + await this.#addSignature( + SignatureRequestType.RevokeInstallations, + signatureText, + this.#signer, + ); + + await this.#applySignatures(); + } + + async revokeInstallations(installationIds: Uint8Array[]) { + const signatureText = + await this.#revokeInstallationsSignatureText(installationIds); if (!signatureText) { throw new Error("Unable to generate revoke installations signature text"); diff --git a/sdks/browser-sdk/src/WorkerClient.ts b/sdks/browser-sdk/src/WorkerClient.ts index 41adbb046..311c271f7 100644 --- a/sdks/browser-sdk/src/WorkerClient.ts +++ b/sdks/browser-sdk/src/WorkerClient.ts @@ -75,9 +75,19 @@ export class WorkerClient { } } - async revokeInstallationsSignatureText() { + async revokeAllAOtherInstallationsSignatureText() { try { - return await this.#client.revokeInstallationsSignatureText(); + return await this.#client.revokeAllOtherInstallationsSignatureText(); + } catch { + return undefined; + } + } + + async revokeInstallationsSignatureText(installationIds: Uint8Array[]) { + try { + return await this.#client.revokeInstallationsSignatureText( + installationIds, + ); } catch { return undefined; } diff --git a/sdks/browser-sdk/src/WorkerConversations.ts b/sdks/browser-sdk/src/WorkerConversations.ts index 0e3163fe8..d927802dd 100644 --- a/sdks/browser-sdk/src/WorkerConversations.ts +++ b/sdks/browser-sdk/src/WorkerConversations.ts @@ -54,28 +54,26 @@ export class WorkerConversations { } } - async list(options?: SafeListConversationsOptions) { - const groups = (await this.#conversations.list( + list(options?: SafeListConversationsOptions) { + const groups = this.#conversations.list( options ? fromSafeListConversationsOptions(options) : undefined, - )) as Conversation[]; + ) as Conversation[]; return groups.map((group) => new WorkerConversation(this.#client, group)); } - async listGroups( + listGroups( options?: Omit, ) { - const groups = (await this.#conversations.listGroups( + const groups = this.#conversations.listGroups( options ? fromSafeListConversationsOptions(options) : undefined, - )) as Conversation[]; + ) as Conversation[]; return groups.map((group) => new WorkerConversation(this.#client, group)); } - async listDms( - options?: Omit, - ) { - const groups = (await this.#conversations.listDms( + listDms(options?: Omit) { + const groups = this.#conversations.listDms( options ? fromSafeListConversationsOptions(options) : undefined, - )) as Conversation[]; + ) as Conversation[]; return groups.map((group) => new WorkerConversation(this.#client, group)); } diff --git a/sdks/browser-sdk/src/types/clientEvents.ts b/sdks/browser-sdk/src/types/clientEvents.ts index 14ffa0303..4507a7c7f 100644 --- a/sdks/browser-sdk/src/types/clientEvents.ts +++ b/sdks/browser-sdk/src/types/clientEvents.ts @@ -69,11 +69,19 @@ export type ClientEvents = }; } | { - action: "revokeInstallationsSignatureText"; + action: "revokeAllOtherInstallationsSignatureText"; id: string; result: string | undefined; data: undefined; } + | { + action: "revokeInstallationsSignatureText"; + id: string; + result: string | undefined; + data: { + installationIds: Uint8Array[]; + }; + } | { action: "addSignature"; id: string; diff --git a/sdks/browser-sdk/src/workers/client.ts b/sdks/browser-sdk/src/workers/client.ts index 65dc68087..9101357f2 100644 --- a/sdks/browser-sdk/src/workers/client.ts +++ b/sdks/browser-sdk/src/workers/client.ts @@ -105,8 +105,19 @@ self.onmessage = async (event: MessageEvent) => { }); break; } + case "revokeAllOtherInstallationsSignatureText": { + const result = await client.revokeAllAOtherInstallationsSignatureText(); + postMessage({ + id, + action, + result, + }); + break; + } case "revokeInstallationsSignatureText": { - const result = await client.revokeInstallationsSignatureText(); + const result = await client.revokeInstallationsSignatureText( + data.installationIds, + ); postMessage({ id, action, @@ -255,7 +266,7 @@ self.onmessage = async (event: MessageEvent) => { * Conversations actions */ case "getConversations": { - const conversations = await client.conversations.list(data.options); + const conversations = client.conversations.list(data.options); postMessage({ id, action, @@ -268,9 +279,7 @@ self.onmessage = async (event: MessageEvent) => { break; } case "getGroups": { - const conversations = await client.conversations.listGroups( - data.options, - ); + const conversations = client.conversations.listGroups(data.options); postMessage({ id, action, @@ -283,7 +292,7 @@ self.onmessage = async (event: MessageEvent) => { break; } case "getDms": { - const conversations = await client.conversations.listDms(data.options); + const conversations = client.conversations.listDms(data.options); postMessage({ id, action, @@ -296,11 +305,6 @@ self.onmessage = async (event: MessageEvent) => { break; } case "newGroup": { - // console.log( - // "newGroup", - // fromSafeCreateGroupOptions(data.options!), - // data.options, - // ); const conversation = await client.conversations.newGroup( data.accountAddresses, data.options, diff --git a/sdks/browser-sdk/test/Client.test.ts b/sdks/browser-sdk/test/Client.test.ts index 74ca45910..e95400475 100644 --- a/sdks/browser-sdk/test/Client.test.ts +++ b/sdks/browser-sdk/test/Client.test.ts @@ -109,7 +109,7 @@ describe.concurrent("Client", () => { ]); }); - it("should revoke all installations", async () => { + it("should revoke all other installations", async () => { const user = createUser(); const client = await createRegisteredClient(user); @@ -126,7 +126,7 @@ describe.concurrent("Client", () => { expect(installationIds).toContain(client2.installationId); expect(installationIds).toContain(client3.installationId); - await client3.revokeInstallations(); + await client3.revokeAllOtherInstallations(); const inboxState2 = await client3.inboxState(true); @@ -134,6 +134,35 @@ describe.concurrent("Client", () => { expect(inboxState2.installations[0].id).toBe(client3.installationId); }); + it("should revoke specific installations", async () => { + const user = createUser(); + + const client = await createRegisteredClient(user); + user.uuid = v4(); + const client2 = await createRegisteredClient(user); + user.uuid = v4(); + const client3 = await createRegisteredClient(user); + + const inboxState = await client3.inboxState(true); + expect(inboxState.installations.length).toBe(3); + + const installationIds = inboxState.installations.map((i) => i.id); + expect(installationIds).toContain(client.installationId); + expect(installationIds).toContain(client2.installationId); + expect(installationIds).toContain(client3.installationId); + + await client3.revokeInstallations([client.installationIdBytes!]); + + const inboxState2 = await client3.inboxState(true); + + expect(inboxState2.installations.length).toBe(2); + + const installationIds2 = inboxState2.installations.map((i) => i.id); + expect(installationIds2).toContain(client2.installationId); + expect(installationIds2).toContain(client3.installationId); + expect(installationIds2).not.toContain(client.installationId); + }); + it("should manage consent states", async () => { const user1 = createUser(); const user2 = createUser(); diff --git a/sdks/node-sdk/package.json b/sdks/node-sdk/package.json index 4b4c0ef3e..0840798bb 100644 --- a/sdks/node-sdk/package.json +++ b/sdks/node-sdk/package.json @@ -51,7 +51,7 @@ "@xmtp/content-type-group-updated": "^2.0.0", "@xmtp/content-type-primitives": "^2.0.0", "@xmtp/content-type-text": "^2.0.0", - "@xmtp/node-bindings": "^0.0.31", + "@xmtp/node-bindings": "^0.0.33", "@xmtp/proto": "^3.72.3" }, "devDependencies": { diff --git a/sdks/node-sdk/src/Client.ts b/sdks/node-sdk/src/Client.ts index b479bc9f2..71a0ce245 100644 --- a/sdks/node-sdk/src/Client.ts +++ b/sdks/node-sdk/src/Client.ts @@ -203,10 +203,22 @@ export class Client { } } - async #revokeInstallationsSignatureText() { + async #revokeAllOtherInstallationsSignatureText() { try { const signatureText = - await this.#innerClient.revokeInstallationsSignatureText(); + await this.#innerClient.revokeAllOtherInstallationsSignatureText(); + return signatureText; + } catch { + return null; + } + } + + async #revokeInstallationsSignatureText(installationIds: Uint8Array[]) { + try { + const signatureText = + await this.#innerClient.revokeInstallationsSignatureText( + installationIds, + ); return signatureText; } catch { return null; @@ -288,8 +300,28 @@ export class Client { await this.#applySignatures(); } - async revokeInstallations() { - const signatureText = await this.#revokeInstallationsSignatureText(); + async revokeAllOtherInstallations() { + const signatureText = + await this.#revokeAllOtherInstallationsSignatureText(); + + if (!signatureText) { + throw new Error( + "Unable to generate revoke all other installations signature text", + ); + } + + await this.#addSignature( + SignatureRequestType.RevokeInstallations, + signatureText, + this.#signer, + ); + + await this.#applySignatures(); + } + + async revokeInstallations(installationIds: Uint8Array[]) { + const signatureText = + await this.#revokeInstallationsSignatureText(installationIds); if (!signatureText) { throw new Error("Unable to generate revoke installations signature text"); diff --git a/sdks/node-sdk/src/Conversations.ts b/sdks/node-sdk/src/Conversations.ts index 586394b03..91afdcd85 100644 --- a/sdks/node-sdk/src/Conversations.ts +++ b/sdks/node-sdk/src/Conversations.ts @@ -62,26 +62,24 @@ export class Conversations { return conversation; } - async list(options?: ListConversationsOptions) { - const groups = await this.#conversations.list(options); + list(options?: ListConversationsOptions) { + const groups = this.#conversations.list(options); return groups.map((group) => { const conversation = new Conversation(this.#client, group); return conversation; }); } - async listGroups( - options?: Omit, - ) { - const groups = await this.#conversations.listGroups(options); + listGroups(options?: Omit) { + const groups = this.#conversations.listGroups(options); return groups.map((group) => { const conversation = new Conversation(this.#client, group); return conversation; }); } - async listDms(options?: Omit) { - const groups = await this.#conversations.listDms(options); + listDms(options?: Omit) { + const groups = this.#conversations.listDms(options); return groups.map((group) => { const conversation = new Conversation(this.#client, group); return conversation; diff --git a/sdks/node-sdk/test/Client.test.ts b/sdks/node-sdk/test/Client.test.ts index 33965c469..332ea315a 100644 --- a/sdks/node-sdk/test/Client.test.ts +++ b/sdks/node-sdk/test/Client.test.ts @@ -135,7 +135,7 @@ describe("Client", () => { ]); }); - it("should revoke all installations", async () => { + it("should revoke all other installations", async () => { const user = createUser(); const client = await createRegisteredClient(user); @@ -152,7 +152,7 @@ describe("Client", () => { expect(installationIds).toContain(client2.installationId); expect(installationIds).toContain(client3.installationId); - await client3.revokeInstallations(); + await client3.revokeAllOtherInstallations(); const inboxState2 = await client3.inboxState(true); @@ -160,6 +160,35 @@ describe("Client", () => { expect(inboxState2.installations[0].id).toBe(client3.installationId); }); + it("should revoke specific installations", async () => { + const user = createUser(); + + const client = await createRegisteredClient(user); + user.uuid = v4(); + const client2 = await createRegisteredClient(user); + user.uuid = v4(); + const client3 = await createRegisteredClient(user); + + const inboxState = await client3.inboxState(true); + expect(inboxState.installations.length).toBe(3); + + const installationIds = inboxState.installations.map((i) => i.id); + expect(installationIds).toContain(client.installationId); + expect(installationIds).toContain(client2.installationId); + expect(installationIds).toContain(client3.installationId); + + await client3.revokeInstallations([client.installationIdBytes]); + + const inboxState2 = await client3.inboxState(true); + + expect(inboxState2.installations.length).toBe(2); + + const installationIds2 = inboxState2.installations.map((i) => i.id); + expect(installationIds2).toContain(client2.installationId); + expect(installationIds2).toContain(client3.installationId); + expect(installationIds2).not.toContain(client.installationId); + }); + it("should manage consent states", async () => { const user1 = createUser(); const user2 = createUser(); diff --git a/sdks/node-sdk/test/Conversation.test.ts b/sdks/node-sdk/test/Conversation.test.ts index b1938bd60..2ad497be3 100644 --- a/sdks/node-sdk/test/Conversation.test.ts +++ b/sdks/node-sdk/test/Conversation.test.ts @@ -28,7 +28,7 @@ describe("Conversation", () => { expect(messages.length).toBe(2); await client2.conversations.sync(); - const conversations = await client2.conversations.list(); + const conversations = client2.conversations.list(); expect(conversations.length).toBe(1); const conversation2 = conversations[0]; @@ -55,7 +55,7 @@ describe("Conversation", () => { expect(messages.length).toBe(2); await client2.conversations.sync(); - const conversations = await client2.conversations.list(); + const conversations = client2.conversations.list(); expect(conversations.length).toBe(1); const conversation2 = conversations[0]; @@ -82,7 +82,7 @@ describe("Conversation", () => { expect(messages.length).toBe(2); await client2.conversations.sync(); - const conversations = await client2.conversations.list(); + const conversations = client2.conversations.list(); expect(conversations.length).toBe(1); const conversation2 = conversations[0]; @@ -109,7 +109,7 @@ describe("Conversation", () => { expect(messages.length).toBe(2); await client2.conversations.sync(); - const conversations = await client2.conversations.list(); + const conversations = client2.conversations.list(); expect(conversations.length).toBe(1); const conversation2 = conversations[0]; @@ -215,7 +215,7 @@ describe("Conversation", () => { expect(messages[1].content).toBe(text); await client2.conversations.sync(); - const conversations = await client2.conversations.list(); + const conversations = client2.conversations.list(); expect(conversations.length).toBe(1); const conversation2 = conversations[0]; @@ -263,7 +263,7 @@ describe("Conversation", () => { expect(messages[1].content).toBe(text); await client2.conversations.sync(); - const conversations = await client2.conversations.list(); + const conversations = client2.conversations.list(); expect(conversations.length).toBe(1); const conversation2 = conversations[0]; @@ -325,7 +325,7 @@ describe("Conversation", () => { ]); await client2.conversations.sync(); - const conversation2 = await client2.conversations.list(); + const conversation2 = client2.conversations.list(); expect(conversation2.length).toBe(1); expect(conversation2[0].id).toBe(conversation.id); diff --git a/sdks/node-sdk/test/Conversations.test.ts b/sdks/node-sdk/test/Conversations.test.ts index 311c278cf..a5feec39f 100644 --- a/sdks/node-sdk/test/Conversations.test.ts +++ b/sdks/node-sdk/test/Conversations.test.ts @@ -7,9 +7,9 @@ describe("Conversations", () => { const user = createUser(); const client = await createRegisteredClient(user); - expect((await client.conversations.list()).length).toBe(0); - expect((await client.conversations.listDms()).length).toBe(0); - expect((await client.conversations.listGroups()).length).toBe(0); + expect(client.conversations.list().length).toBe(0); + expect(client.conversations.listDms().length).toBe(0); + expect(client.conversations.listGroups().length).toBe(0); }); it("should create a group", async () => { @@ -55,20 +55,20 @@ describe("Conversations", () => { creatorInboxId: client1.inboxId, }); - const conversations1 = await client1.conversations.list(); + const conversations1 = client1.conversations.list(); expect(conversations1.length).toBe(1); expect(conversations1[0].id).toBe(conversation.id); - expect((await client2.conversations.list()).length).toBe(0); + expect(client2.conversations.list().length).toBe(0); await client2.conversations.sync(); - const conversations2 = await client2.conversations.list(); + const conversations2 = client2.conversations.list(); expect(conversations2.length).toBe(1); expect(conversations2[0].id).toBe(conversation.id); - expect((await client2.conversations.listDms()).length).toBe(0); - expect((await client2.conversations.listGroups()).length).toBe(1); + expect(client2.conversations.listDms().length).toBe(0); + expect(client2.conversations.listGroups().length).toBe(1); }); it("should create a dm", async () => { @@ -108,25 +108,25 @@ describe("Conversations", () => { expect(group.consentState).toBe(ConsentState.Allowed); - const group1 = await client1.conversations.list(); + const group1 = client1.conversations.list(); expect(group1.length).toBe(1); expect(group1[0].id).toBe(group.id); expect(group1[0].dmPeerInboxId).toBe(client2.inboxId); - expect((await client1.conversations.listDms()).length).toBe(1); - expect((await client1.conversations.listGroups()).length).toBe(0); + expect(client1.conversations.listDms().length).toBe(1); + expect(client1.conversations.listGroups().length).toBe(0); - expect((await client2.conversations.list()).length).toBe(0); + expect(client2.conversations.list().length).toBe(0); await client2.conversations.sync(); - const group2 = await client2.conversations.list(); + const group2 = client2.conversations.list(); expect(group2.length).toBe(1); expect(group2[0].id).toBe(group.id); expect(group2[0].dmPeerInboxId).toBe(client1.inboxId); - expect((await client2.conversations.listDms()).length).toBe(1); - expect((await client2.conversations.listGroups()).length).toBe(0); + expect(client2.conversations.listDms().length).toBe(1); + expect(client2.conversations.listGroups().length).toBe(0); const dm1 = client1.conversations.getDmByInboxId(client2.inboxId); expect(dm1).toBeDefined(); @@ -394,10 +394,10 @@ describe("Conversations", () => { const stream = await client1.conversations.streamAllMessages(); await client2.conversations.sync(); - const groups2 = await client2.conversations.list(); + const groups2 = client2.conversations.list(); await client3.conversations.sync(); - const groups3 = await client3.conversations.list(); + const groups3 = client3.conversations.list(); await groups2[0].send("gm!"); await groups3[0].send("gm2!"); @@ -434,15 +434,15 @@ describe("Conversations", () => { const groups2 = client2.conversations; await groups2.sync(); - const groupsList2 = await groups2.list(); + const groupsList2 = groups2.list(); const groups3 = client3.conversations; await groups3.sync(); - const groupsList3 = await groups3.list(); + const groupsList3 = groups3.list(); const groups4 = client4.conversations; await groups4.sync(); - const groupsList4 = await groups4.list(); + const groupsList4 = groups4.list(); await groupsList4[0].send("gm3!"); await groupsList2[0].send("gm!"); @@ -480,15 +480,15 @@ describe("Conversations", () => { const groups2 = client2.conversations; await groups2.sync(); - const groupsList2 = await groups2.list(); + const groupsList2 = groups2.list(); const groups3 = client3.conversations; await groups3.sync(); - const groupsList3 = await groups3.list(); + const groupsList3 = groups3.list(); const groups4 = client4.conversations; await groups4.sync(); - const groupsList4 = await groups4.list(); + const groupsList4 = groups4.list(); await groupsList2[0].send("gm!"); await groupsList3[0].send("gm2!"); diff --git a/yarn.lock b/yarn.lock index fd297f394..c3acf5bc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4809,7 +4809,7 @@ __metadata: "@xmtp/content-type-primitives": "npm:^2.0.0" "@xmtp/content-type-text": "npm:^2.0.0" "@xmtp/proto": "npm:^3.72.3" - "@xmtp/wasm-bindings": "npm:^0.0.9" + "@xmtp/wasm-bindings": "npm:^0.0.11" playwright: "npm:^1.49.0" rollup: "npm:^4.27.3" rollup-plugin-dts: "npm:^6.1.1" @@ -5114,10 +5114,10 @@ __metadata: languageName: node linkType: hard -"@xmtp/node-bindings@npm:^0.0.31": - version: 0.0.31 - resolution: "@xmtp/node-bindings@npm:0.0.31" - checksum: 10/bcb754e23a8dd123789580d288336343e5bed12f2952ea24a0ee24961494c8765469f14b836815a33d54a7fe35afe2edc72c8113487013a690888eb190790a3f +"@xmtp/node-bindings@npm:^0.0.33": + version: 0.0.33 + resolution: "@xmtp/node-bindings@npm:0.0.33" + checksum: 10/ba4e9b16fc261fa5ec3dc8b934074347e28e75590e85d113c339579c9eebc58b8cb2114f113106ab111347793d739aa41efa855013a5a7919240344869a58a75 languageName: node linkType: hard @@ -5145,7 +5145,7 @@ __metadata: "@xmtp/content-type-group-updated": "npm:^2.0.0" "@xmtp/content-type-primitives": "npm:^2.0.0" "@xmtp/content-type-text": "npm:^2.0.0" - "@xmtp/node-bindings": "npm:^0.0.31" + "@xmtp/node-bindings": "npm:^0.0.33" "@xmtp/proto": "npm:^3.72.3" "@xmtp/xmtp-js": "workspace:^" fast-glob: "npm:^3.3.2" @@ -5256,10 +5256,10 @@ __metadata: languageName: node linkType: hard -"@xmtp/wasm-bindings@npm:^0.0.9": - version: 0.0.9 - resolution: "@xmtp/wasm-bindings@npm:0.0.9" - checksum: 10/02cb19cf4b610b040a52e48b324e712a84b6278834c68ce0f0f7d033a00e2188b58cc75c69a30c0b908505921cd9ef354bce51528105267141320378681280d1 +"@xmtp/wasm-bindings@npm:^0.0.11": + version: 0.0.11 + resolution: "@xmtp/wasm-bindings@npm:0.0.11" + checksum: 10/6482ca317888e4d1ff53c4d9e201bd81c26b67bdcbe13a09615f76ce3ebd503535e52f998c414ee6e4675b1a5396b73dd70db5a0fb7536475760a16d796e40c1 languageName: node linkType: hard