diff --git a/apps/server/src/auth/PairingGrantStore.test.ts b/apps/server/src/auth/PairingGrantStore.test.ts index 5184beb096807..3f95ce46ec695 100644 --- a/apps/server/src/auth/PairingGrantStore.test.ts +++ b/apps/server/src/auth/PairingGrantStore.test.ts @@ -70,6 +70,22 @@ it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("PairingGrantStor ), ); + it.effect("accepts a one-time token typed in hyphen- or space-separated groups", () => + Effect.gen(function* () { + const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore; + const issued = yield* bootstrapCredentials.issueOneTimeToken(); + const formatted = issued.credential.match(/.{1,4}/g)!.join("-"); + const consumed = yield* bootstrapCredentials.consume(formatted.toLowerCase()); + const second = yield* bootstrapCredentials.issueOneTimeToken(); + const spaced = yield* bootstrapCredentials.consume( + second.credential.match(/.{1,4}/g)!.join(" "), + ); + + expect(consumed.method).toBe("one-time-token"); + expect(spaced.method).toBe("one-time-token"); + }).pipe(Effect.provide(layerPairingGrantStore())), + ); + it.effect("atomically consumes a one-time token when multiple requests race", () => Effect.gen(function* () { const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore; diff --git a/apps/server/src/auth/PairingGrantStore.ts b/apps/server/src/auth/PairingGrantStore.ts index bf9eccee8c51b..7af7f9ebc7a31 100644 --- a/apps/server/src/auth/PairingGrantStore.ts +++ b/apps/server/src/auth/PairingGrantStore.ts @@ -561,8 +561,10 @@ const make = Effect.gen(function* () { } // Only credentials in the issued pairing-code formats are case-insensitive. - const pairingCredential = PAIRING_TOKEN_PATTERN.test(credential) - ? credential.toUpperCase() + // People type codes in groups such as XXXX-XXXX-XXXX. + const compactCredential = credential.replace(/[\s-]/g, ""); + const pairingCredential = PAIRING_TOKEN_PATTERN.test(compactCredential) + ? compactCredential.toUpperCase() : (parseMcpApprovalCode(credential)?.code ?? credential); // Check scopes in the UPDATE so rejected requests cannot spend one-time links.