From 7676061314659be609faec1f3f23c2e6de652059 Mon Sep 17 00:00:00 2001 From: Mitsuki Ogasahara Date: Wed, 22 Apr 2026 21:41:50 +0900 Subject: [PATCH] fix(scim-users): use SCIM filter path when updating email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slack's SCIM PATCH rejects `replace` with path=emails and an array value containing a primary entry: SCIM API error (400): Multi-valued attributes can not have more than one primary element RFC 7644 ยง3.5.2 specifies that `replace` on a multi-valued attribute replaces the whole array, but Slack's implementation appears to treat it as an add/merge. Using the filter path `emails[primary eq true].value` updates only the primary email's value and is accepted. Verified against a live Enterprise Grid workspace. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/commands/scim-users/update.ts | 2 +- tests/commands/scim-users/update.test.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/commands/scim-users/update.ts b/src/commands/scim-users/update.ts index 967a328..02ff05f 100644 --- a/src/commands/scim-users/update.ts +++ b/src/commands/scim-users/update.ts @@ -20,7 +20,7 @@ export async function executeScimUsersUpdate( if (opts.active !== undefined) operations.push({ op: "replace", path: "active", value: opts.active }); if (opts.userName !== undefined) operations.push({ op: "replace", path: "userName", value: opts.userName }); - if (opts.email !== undefined) operations.push({ op: "replace", path: "emails", value: [{ value: opts.email, primary: true }] }); + if (opts.email !== undefined) operations.push({ op: "replace", path: 'emails[primary eq true].value', value: opts.email }); if (opts.givenName !== undefined) operations.push({ op: "replace", path: "name.givenName", value: opts.givenName }); if (opts.familyName !== undefined) operations.push({ op: "replace", path: "name.familyName", value: opts.familyName }); if (opts.displayName !== undefined) operations.push({ op: "replace", path: "displayName", value: opts.displayName }); diff --git a/tests/commands/scim-users/update.test.ts b/tests/commands/scim-users/update.test.ts index 5b13b6f..4cc59ed 100644 --- a/tests/commands/scim-users/update.test.ts +++ b/tests/commands/scim-users/update.test.ts @@ -24,6 +24,16 @@ describe("scim-users update", () => { ]); }); + test("updates email using SCIM filter path for primary address", async () => { + const updated = { id: "U001", emails: [{ value: "new@example.com", primary: true }] }; + const mockUpdate = mock(() => Promise.resolve(updated)); + const client = { users: { update: mockUpdate } } as any; + await executeScimUsersUpdate(client, { id: "U001", email: "new@example.com" }); + expect(mockUpdate).toHaveBeenCalledWith("U001", [ + { op: "replace", path: 'emails[primary eq true].value', value: "new@example.com" }, + ]); + }); + test("throws when no fields provided", async () => { const client = { users: { update: mock(() => Promise.resolve({})) } } as any; await expect(executeScimUsersUpdate(client, { id: "U001" })).rejects.toThrow(