diff --git a/packages/auth/src/infrastructure/repositories/users.repository.mock.ts b/packages/auth/src/infrastructure/repositories/users.repository.mock.ts index 0f0417a..ca5910c 100644 --- a/packages/auth/src/infrastructure/repositories/users.repository.mock.ts +++ b/packages/auth/src/infrastructure/repositories/users.repository.mock.ts @@ -48,7 +48,9 @@ export class MockUsersRepository implements IUsersRepository { { name: "users.getUserByUsername", op: "repository", - attributes: { emailDomain: username.includes("@") ? (username.split("@")[1] ?? "(invalid)") : username }, + // Never emit the username (or any slice of it) — it is PII and the + // non-email branch used to leak the full username (audit finding B6). + attributes: { hasAtSign: username.includes("@") }, }, async (span) => { const found = this._users.find((u) => u.username === username); @@ -60,7 +62,11 @@ export class MockUsersRepository implements IUsersRepository { async createUser(input: User): Promise { return this.tracer.startSpan( - { name: "users.createUser", op: "repository", attributes: { id: input.id } }, + { + name: "users.createUser", + op: "repository", + attributes: { id: input.id }, + }, async (span) => { this._users.push(input); span.setAttribute("created", true); diff --git a/packages/auth/src/infrastructure/repositories/users.repository.span.test.ts b/packages/auth/src/infrastructure/repositories/users.repository.span.test.ts index d8ae991..d9ef6b6 100644 --- a/packages/auth/src/infrastructure/repositories/users.repository.span.test.ts +++ b/packages/auth/src/infrastructure/repositories/users.repository.span.test.ts @@ -21,17 +21,31 @@ describe("MockUsersRepository emits spans", () => { expect(tracer.spans[0]!.attributes.found).toBe(false); }); - it("getUserByUsername emits a span with emailDomain attribute", async () => { + it("getUserByUsername emits a span without any username-derived PII", async () => { const tracer = new RecordingTracer(); const repo = new MockUsersRepository( [{ id: "1", username: "alice", passwordHash: "hash" }], tracer, ); await repo.getUserByUsername("alice"); - expect(tracer.findSpan("users.getUserByUsername")).toBeDefined(); - expect(tracer.findSpan("users.getUserByUsername")!.attributes.found).toBe( - true, - ); + const span = tracer.findSpan("users.getUserByUsername"); + expect(span).toBeDefined(); + expect(span!.attributes.found).toBe(true); + // B6 regression guard: the old emailDomain attribute leaked the full + // username when it contained no "@". Only a boolean may be emitted. + expect(span!.attributes.emailDomain).toBeUndefined(); + expect(span!.attributes.hasAtSign).toBe(false); + expect(Object.values(span!.attributes)).not.toContain("alice"); + }); + + it("getUserByUsername with an email-shaped username emits only the boolean", async () => { + const tracer = new RecordingTracer(); + const repo = new MockUsersRepository([], tracer); + await repo.getUserByUsername("alice@example.com"); + const span = tracer.findSpan("users.getUserByUsername"); + expect(span!.attributes.hasAtSign).toBe(true); + expect(span!.attributes.emailDomain).toBeUndefined(); + expect(Object.values(span!.attributes)).not.toContain("example.com"); }); it("createUser records created=true", async () => { diff --git a/packages/auth/src/infrastructure/repositories/users.repository.ts b/packages/auth/src/infrastructure/repositories/users.repository.ts index f8b8ecf..eb61db5 100644 --- a/packages/auth/src/infrastructure/repositories/users.repository.ts +++ b/packages/auth/src/infrastructure/repositories/users.repository.ts @@ -79,11 +79,9 @@ export class UsersRepository implements IUsersRepository { { name: "users.getUserByUsername", op: "repository", - attributes: { - emailDomain: username.includes("@") - ? (username.split("@")[1] ?? "(invalid)") - : username, - }, + // Never emit the username (or any slice of it) — it is PII and the + // non-email branch used to leak the full username (audit finding B6). + attributes: { hasAtSign: username.includes("@") }, }, async (span) => { try {